mailgun-ruby 1.1.2 → 1.1.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -23,17 +23,24 @@ module Mailgun
23
23
 
24
24
  # Adds a specific type of recipient to the message object.
25
25
  #
26
+ # WARNING: Setting 'h:reply-to' with add_recipient() is deprecated! Use 'reply_to' instead.
27
+ #
26
28
  # @param [String] recipient_type The type of recipient. "to", "cc", "bcc" or "h:reply-to".
27
29
  # @param [String] address The email address of the recipient to add to the message object.
28
30
  # @param [Hash] variables A hash of the variables associated with the recipient. We recommend "first" and "last" at a minimum!
29
31
  # @return [void]
30
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
+
31
38
  if (@counters[:recipients][recipient_type] || 0) >= Mailgun::Chains::MAX_RECIPIENTS
32
39
  fail Mailgun::ParameterError, 'Too many recipients added to message.', address
33
40
  end
34
41
 
35
42
  compiled_address = parse_address(address, variables)
36
- complex_setter(recipient_type, compiled_address)
43
+ set_multi_complex(recipient_type, compiled_address)
37
44
 
38
45
  @counters[:recipients][recipient_type] += 1 if @counters[:recipients].key?(recipient_type)
39
46
  end
@@ -53,12 +60,25 @@ module Mailgun
53
60
  from(address, variables)
54
61
  end
55
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
+
56
76
  # Set a subject for the message object
57
77
  #
58
78
  # @param [String] subject The subject for the email.
59
79
  # @return [void]
60
80
  def subject(subj = nil)
61
- simple_setter(:subject, subj)
81
+ set_multi_simple(:subject, subj)
62
82
  end
63
83
 
64
84
  # Deprecated: Please use "subject" instead.
@@ -72,7 +92,7 @@ module Mailgun
72
92
  # @param [String] text_body The text body for the email.
73
93
  # @return [void]
74
94
  def body_text(text_body = nil)
75
- simple_setter(:text, text_body)
95
+ set_multi_simple(:text, text_body)
76
96
  end
77
97
 
78
98
  # Deprecated: Please use "body_text" instead.
@@ -86,7 +106,7 @@ module Mailgun
86
106
  # @param [String] html_body The html body for the email.
87
107
  # @return [void]
88
108
  def body_html(html_body = nil)
89
- simple_setter(:html, html_body)
109
+ set_multi_simple(:html, html_body)
90
110
  end
91
111
 
92
112
  # Deprecated: Please use "body_html" instead.
@@ -97,7 +117,7 @@ module Mailgun
97
117
 
98
118
  # Adds a series of attachments, when called upon.
99
119
  #
100
- # @param [String] attachment A file object for attaching as an attachment.
120
+ # @param [String|File] attachment A file object for attaching as an attachment.
101
121
  # @param [String] filename The filename you wish the attachment to be.
102
122
  # @return [void]
103
123
  def add_attachment(attachment, filename = nil)
@@ -106,19 +126,27 @@ module Mailgun
106
126
 
107
127
  # Adds an inline image to the mesage object.
108
128
  #
109
- # @param [String] inline_image A file object for attaching an inline image.
129
+ # @param [String|File] inline_image A file object for attaching an inline image.
110
130
  # @param [String] filename The filename you wish the inline image to be.
111
131
  # @return [void]
112
132
  def add_inline_image(inline_image, filename = nil)
113
133
  add_file(:inline, inline_image, filename)
114
134
  end
115
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
+
116
144
  # Send a message in test mode. (The message won't really be sent to the recipient)
117
145
  #
118
146
  # @param [Boolean] mode The boolean or string value (will fix itself)
119
147
  # @return [void]
120
148
  def test_mode(mode)
121
- simple_setter('o:testmode', bool_lookup(mode))
149
+ set_multi_simple('o:testmode', bool_lookup(mode))
122
150
  end
123
151
 
124
152
  # Deprecated: 'set_test_mode' is depreciated. Please use 'test_mode' instead.
@@ -132,7 +160,7 @@ module Mailgun
132
160
  # @param [Boolean] mode The boolean or string value(will fix itself)
133
161
  # @return [void]
134
162
  def dkim(mode)
135
- simple_setter('o:dkim', bool_lookup(mode))
163
+ set_multi_simple('o:dkim', bool_lookup(mode))
136
164
  end
137
165
 
138
166
  # Deprecated: 'set_dkim' is deprecated. Please use 'dkim' instead.
@@ -148,7 +176,7 @@ module Mailgun
148
176
  def add_campaign_id(campaign_id)
149
177
  fail(Mailgun::ParameterError, 'Too many campaigns added to message.', campaign_id) if @counters[:attributes][:campaign_id] >= Mailgun::Chains::MAX_CAMPAIGN_IDS
150
178
 
151
- complex_setter('o:campaign', campaign_id)
179
+ set_multi_complex('o:campaign', campaign_id)
152
180
  @counters[:attributes][:campaign_id] += 1
153
181
  end
154
182
 
@@ -160,7 +188,7 @@ module Mailgun
160
188
  if @counters[:attributes][:tag] >= Mailgun::Chains::MAX_TAGS
161
189
  fail Mailgun::ParameterError, 'Too many tags added to message.', tag
162
190
  end
163
- complex_setter('o:tag', tag)
191
+ set_multi_complex('o:tag', tag)
164
192
  @counters[:attributes][:tag] += 1
165
193
  end
166
194
 
@@ -169,7 +197,7 @@ module Mailgun
169
197
  # @param [Boolean] tracking Boolean true or false.
170
198
  # @return [void]
171
199
  def track_opens(mode)
172
- simple_setter('o:tracking-opens', bool_lookup(mode))
200
+ set_multi_simple('o:tracking-opens', bool_lookup(mode))
173
201
  end
174
202
 
175
203
  # Deprecated: 'set_open_tracking' is deprecated. Please use 'track_opens' instead.
@@ -183,7 +211,7 @@ module Mailgun
183
211
  # @param [String] mode True, False, or HTML (for HTML only tracking)
184
212
  # @return [void]
185
213
  def track_clicks(mode)
186
- simple_setter('o:tracking-clicks', bool_lookup(mode))
214
+ set_multi_simple('o:tracking-clicks', bool_lookup(mode))
187
215
  end
188
216
 
189
217
  # Depreciated: 'set_click_tracking. is deprecated. Please use 'track_clicks' instead.
@@ -201,7 +229,7 @@ module Mailgun
201
229
  # @return [void]
202
230
  def deliver_at(timestamp)
203
231
  time_str = DateTime.parse(timestamp)
204
- simple_setter('o:deliverytime', time_str.rfc2822)
232
+ set_multi_simple('o:deliverytime', time_str.rfc2822)
205
233
  end
206
234
 
207
235
  # Deprecated: 'set_delivery_time' is deprecated. Please use 'deliver_at' instead.
@@ -218,8 +246,12 @@ module Mailgun
218
246
  # @return [void]
219
247
  def header(name, data)
220
248
  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)
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
223
255
  end
224
256
 
225
257
  # Deprecated: 'set_custom_data' is deprecated. Please use 'header' instead.
@@ -228,6 +260,19 @@ module Mailgun
228
260
  header name, data
229
261
  end
230
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
+
231
276
  # Add custom parameter to the message. A custom parameter is any parameter that
232
277
  # is not yet supported by the SDK, but available at the API. Note: No validation
233
278
  # is performed. Don't forget to prefix the parameter with o, h, or v.
@@ -236,7 +281,7 @@ module Mailgun
236
281
  # @param [string] data A string of data for the parameter.
237
282
  # @return [void]
238
283
  def add_custom_parameter(name, data)
239
- complex_setter(name, data)
284
+ set_multi_complex(name, data)
240
285
  end
241
286
 
242
287
  # Set the Message-Id header to a custom value. Don't forget to enclose the
@@ -249,7 +294,7 @@ module Mailgun
249
294
  def message_id(data = nil)
250
295
  key = 'h:Message-Id'
251
296
  return @message.delete(key) if data.to_s.empty?
252
- @message[key] = data
297
+ set_single(key, data)
253
298
  end
254
299
 
255
300
  # Deprecated: 'set_message_id' is deprecated. Use 'message_id' instead.
@@ -260,13 +305,23 @@ module Mailgun
260
305
 
261
306
  private
262
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
+
263
318
  # Sets values within the multidict, however, prevents
264
319
  # duplicate values for keys.
265
320
  #
266
321
  # @param [String] parameter The message object parameter name.
267
322
  # @param [String] value The value of the parameter.
268
323
  # @return [void]
269
- def simple_setter(parameter, value)
324
+ def set_multi_simple(parameter, value)
270
325
  @message[parameter] = value ? [value] : ['']
271
326
  end
272
327
 
@@ -276,7 +331,7 @@ module Mailgun
276
331
  # @param [String] parameter The message object parameter name.
277
332
  # @param [String] value The value of the parameter.
278
333
  # @return [void]
279
- def complex_setter(parameter, value)
334
+ def set_multi_complex(parameter, value)
280
335
  @message[parameter] << (value || '')
281
336
  end
282
337
 
@@ -307,9 +362,9 @@ module Mailgun
307
362
  #
308
363
  # Returns a JSON object or raises ParameterError
309
364
  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
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
313
368
  rescue
314
369
  raise Mailgun::ParameterError, 'Provided data could not be made into JSON. Try a JSON string or Hash.', obj
315
370
  end
@@ -334,7 +389,7 @@ module Mailgun
334
389
  # Private: Adds a file to the message.
335
390
  #
336
391
  # @param [Symbol] disposition The type of file: :attachment or :inline
337
- # @param [String] attachment A file object for attaching as an attachment.
392
+ # @param [String|File] attachment A file object for attaching as an attachment.
338
393
  # @param [String] filename The filename you wish the attachment to be.
339
394
  # @return [void]
340
395
  #
@@ -351,7 +406,7 @@ module Mailgun
351
406
  attachment.instance_variable_set :@original_filename, filename
352
407
  attachment.instance_eval 'def original_filename; @original_filename; end'
353
408
  end
354
- complex_setter(disposition, attachment)
409
+ set_multi_complex(disposition, attachment)
355
410
  end
356
411
  end
357
412
 
@@ -0,0 +1,270 @@
1
+ require 'uri'
2
+
3
+ require 'mailgun/exceptions/exceptions'
4
+
5
+ module Mailgun
6
+
7
+ # The Mailgun::Suppressions object makes it easy to manage "suppressions"
8
+ # attached to an account. "Suppressions" means bounces, unsubscribes, and complaints.
9
+ class Suppressions
10
+
11
+ # @param [Mailgun::Client] client API client to use for requests
12
+ # @param [String] domain Domain name to use for the suppression endpoints.
13
+ def initialize(client, domain)
14
+ @client = client
15
+ @domain = domain
16
+
17
+ @paging_next = nil
18
+ @paging_prev = nil
19
+ end
20
+
21
+ ####
22
+ # Paging operations
23
+ ####
24
+
25
+ def next
26
+ response = get_from_paging @paging_next[:path], @paging_next[:params]
27
+ extract_paging response
28
+ response
29
+ end
30
+
31
+ def prev
32
+ response = get_from_paging @paging_prev[:path], @paging_prev[:params]
33
+ extract_paging response
34
+ response
35
+ end
36
+
37
+ ####
38
+ # Bounces Endpoint (/v3/:domain/bounces)
39
+ ####
40
+
41
+ def list_bounces(params = {})
42
+ response = @client.get("#{@domain}/bounces", params)
43
+ extract_paging response
44
+ response
45
+ end
46
+
47
+ def get_bounce(address)
48
+ @client.get("#{@domain}/bounces/#{address}", nil)
49
+ end
50
+
51
+ def create_bounce(params = {})
52
+ @client.post("#{@domain/bounces}", params)
53
+ end
54
+
55
+ # Creates multiple bounces on the Mailgun API.
56
+ # If a bounce does not have a valid structure, it will be added to a list of unsendable bounces.
57
+ # The list of unsendable bounces will be returned at the end of this operation.
58
+ #
59
+ # If more than 999 bounce entries are provided, the list will be split and recursive calls will be made.
60
+ #
61
+ # @param [Array] data Array of bounce hashes
62
+ # @return [Response] Mailgun API response
63
+ # @return [Array] Return values from recursive call for list split.
64
+ def create_bounces(data)
65
+ # `data` should be a list of hashes, with each hash containing *at least* an `address` key.
66
+ split_return = []
67
+ if data.length >= 1000 then
68
+ resp, resp_l = create_bounces data[999..-1]
69
+ split_return.push(resp)
70
+ split_return.concat(resp_l)
71
+ data = data[0..998]
72
+ elsif data.length == 0 then
73
+ return nil, []
74
+ end
75
+
76
+ valid = []
77
+ # Validate the bounces given
78
+ # NOTE: `data` could potentially be very large (1000 elements) so it is
79
+ # more efficient to pop from data and push into a different array as
80
+ # opposed to possibly copying the entire array to another array.
81
+ while not data.empty? do
82
+ bounce = data.pop
83
+ # Bounces MUST contain a `address` key.
84
+ if not bounce.include? :address then
85
+ raise Mailgun::ParameterError.new "Bounce MUST include a :address key: #{bounce}"
86
+ end
87
+
88
+ bounce.each do |k, v|
89
+ # Hash values MUST be strings.
90
+ if not v.is_a? String then
91
+ bounce[k] = v.to_s
92
+ end
93
+ end
94
+
95
+ valid.push bounce
96
+ end
97
+
98
+ response = @client.post("#{@domain}/bounces", valid.to_json, { "Content-Type" => "application/json" })
99
+ return response, split_return
100
+ end
101
+
102
+ def delete_bounce(address)
103
+ @client.delete("#{@domain}/bounces/#{address}")
104
+ end
105
+
106
+ def delete_all_bounces
107
+ @client.delete("#{@domain}/bounces")
108
+ end
109
+
110
+ ####
111
+ # Unsubscribes Endpoint (/v3/:domain/unsubscribes)
112
+ ####
113
+
114
+ def list_unsubscribes(params = {})
115
+ response = @client.get("#{@domain}/unsubscribes", params)
116
+ extract_paging response
117
+ response
118
+ end
119
+
120
+ def get_unsubscribe(address)
121
+ @client.get("#{@domain}/unsubscribes/#{address}")
122
+ end
123
+
124
+ def create_unsubscribe(params = {})
125
+ @client.post("#{@domain}/unsubscribes", params)
126
+ end
127
+
128
+ # Creates multiple unsubscribes on the Mailgun API.
129
+ # If an unsubscribe does not have a valid structure, it will be added to a list of unsendable unsubscribes.
130
+ # The list of unsendable unsubscribes will be returned at the end of this operation.
131
+ #
132
+ # If more than 999 unsubscribe entries are provided, the list will be split and recursive calls will be made.
133
+ #
134
+ # @param [Array] data Array of unsubscribe hashes
135
+ # @return [Response] Mailgun API response
136
+ # @return [Array] Return values from recursive call for list split.
137
+ def create_unsubscribes(data)
138
+ # `data` should be a list of hashes, with each hash containing *at least* an `address` key.
139
+ split_return = []
140
+ if data.length >= 1000 then
141
+ resp, resp_l = create_unsubscribes data[999..-1]
142
+ split_return.push(resp)
143
+ split_return.concat(resp_l)
144
+ data = data[0..998]
145
+ elsif data.length == 0 then
146
+ return nil, []
147
+ end
148
+
149
+ valid = []
150
+ # Validate the unsubscribes given
151
+ while not data.empty? do
152
+ unsubscribe = data.pop
153
+ # unsubscribes MUST contain a `address` key.
154
+ if not unsubscribe.include? :address then
155
+ raise Mailgun::ParameterError.new "Unsubscribe MUST include a :address key: #{unsubscribe}"
156
+ end
157
+
158
+ unsubscribe.each do |k, v|
159
+ # Hash values MUST be strings.
160
+ if not v.is_a? String then
161
+ unsubscribe[k] = v.to_s
162
+ end
163
+ end
164
+
165
+ valid.push unsubscribe
166
+ end
167
+
168
+ response = @client.post("#{@domain}/unsubscribes", valid.to_json, { "Content-Type" => "application/json" })
169
+ return response, split_return
170
+ end
171
+
172
+ def delete_unsubscribe(address, params = {})
173
+ @client.delete("#{@domain}/unsubscribes/#{address}")
174
+ end
175
+
176
+ ####
177
+ # Complaints Endpoint (/v3/:domain/complaints)
178
+ ####
179
+
180
+ def list_complaints(params = {})
181
+ response = @client.get("#{@domain}/complaints", params)
182
+ extract_paging response
183
+ response
184
+ end
185
+
186
+ def get_complaint(address)
187
+ @client.get("#{@domain}/complaints/#{address}", nil)
188
+ end
189
+
190
+ def create_complaint(params = {})
191
+ @client.post("#{@domain}/complaints", params)
192
+ end
193
+
194
+ # Creates multiple complaints on the Mailgun API.
195
+ # If a complaint does not have a valid structure, it will be added to a list of unsendable complaints.
196
+ # The list of unsendable complaints will be returned at the end of this operation.
197
+ #
198
+ # If more than 999 complaint entries are provided, the list will be split and recursive calls will be made.
199
+ #
200
+ # @param [Array] data Array of complaint hashes
201
+ # @return [Response] Mailgun API response
202
+ # @return [Array] Return values from recursive call for list split.
203
+ def create_complaints(data)
204
+ # `data` should be a list of hashes, with each hash containing *at least* an `address` key.
205
+ split_return = []
206
+ if data.length >= 1000 then
207
+ resp, resp_l = create_complaints data[999..-1]
208
+ split_return.push(resp)
209
+ split_return.concat(resp_l)
210
+ data = data[0..998]
211
+ elsif data.length == 0 then
212
+ return nil, []
213
+ end
214
+
215
+ valid = []
216
+ # Validate the complaints given
217
+ while not data.empty? do
218
+ complaint = data.pop
219
+ # complaints MUST contain a `address` key.
220
+ if not complaint.include? :address then
221
+ raise Mailgun::ParameterError.new "Complaint MUST include a :address key: #{complaint}"
222
+ end
223
+
224
+ complaint.each do |k, v|
225
+ # Hash values MUST be strings.
226
+ if not v.is_a? String then
227
+ complaint[k] = v.to_s
228
+ end
229
+ end
230
+
231
+ valid.push complaint
232
+ end
233
+
234
+ response = @client.post("#{@domain}/complaints", valid.to_json, { "Content-Type" => "application/json" })
235
+ return response, split_return
236
+ end
237
+
238
+ def delete_complaint(address)
239
+ @client.delete("#{@domain}/complaints/#{address}")
240
+ end
241
+
242
+ private
243
+
244
+ def get_from_paging(uri, params = {})
245
+ @client.get(uri, params)
246
+ end
247
+
248
+ def extract_paging(response)
249
+ rhash = response.to_h
250
+ return nil unless rhash.include? "paging"
251
+
252
+ page_info = rhash["paging"]
253
+
254
+ # Build the `next` endpoint
255
+ page_next = URI.parse(page_info["next"])
256
+ @paging_next = {
257
+ :path => page_next.path[/\/v[\d](.+)/, 1],
258
+ :params => Hash[URI.decode_www_form page_next.query],
259
+ }
260
+
261
+ # Build the `prev` endpoint
262
+ page_prev = URI.parse(page_info["previous"])
263
+ @paging_prev = {
264
+ :path => page_prev.path[/\/v[\d](.+)/, 1],
265
+ :params => Hash[URI.decode_www_form page_prev.query],
266
+ }
267
+ end
268
+
269
+ end
270
+ end
@@ -1,4 +1,4 @@
1
1
  # It's the version. Yeay!
2
2
  module Mailgun
3
- VERSION = '1.1.2'
3
+ VERSION = '1.1.6'
4
4
  end
data/lib/mailgun-ruby.rb CHANGED
@@ -1 +1,2 @@
1
- require 'mailgun'
1
+ require 'mailgun'
2
+ require 'railgun' if defined?(Rails)
@@ -0,0 +1,56 @@
1
+ module Railgun
2
+
3
+ class Attachment < StringIO
4
+
5
+ attr_reader :filename, :content_type, :path,
6
+ :original_filename, :overwritten_filename
7
+
8
+ def initialize(attachment, *args)
9
+ @path = ''
10
+ @inline = args.detect { |opt| opt[:inline] }
11
+
12
+ if @inline
13
+ @filename = attachment.cid
14
+ else
15
+ @filename = attachment.filename
16
+ end
17
+
18
+ @original_filename = @filename
19
+
20
+ if args.detect { |opt| opt[:filename] }
21
+ @filename = opt[:filename]
22
+ end
23
+
24
+ @overwritten_filename = @filename
25
+
26
+ @content_type = attachment.content_type.split(';')[0]
27
+
28
+ super attachment.body.decoded
29
+ end
30
+
31
+ def inline?
32
+ @inline
33
+ end
34
+
35
+ def is_original_filename
36
+ @original_filename == @overwritten_filename
37
+ end
38
+
39
+ def source_filename
40
+ @filename
41
+ end
42
+
43
+ def attach_to_message!(mb)
44
+ if mb.nil?
45
+ nil
46
+ end
47
+
48
+ if inline?
49
+ mb.add_inline_image self, @filename
50
+ else
51
+ mb.add_attachment self, @filename
52
+ end
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,27 @@
1
+ module Railgun
2
+
3
+ class Error < StandardError
4
+
5
+ attr_reader :object
6
+
7
+ def initialize(message = nil, object = nil)
8
+ super(message)
9
+
10
+ @object = object
11
+ end
12
+ end
13
+
14
+ class ConfigurationError < Error
15
+ end
16
+
17
+ class InternalError < Error
18
+
19
+ attr_reader :source_exception
20
+
21
+ def initialize(source_exc, message = nil, object = nil)
22
+ super(message, object)
23
+
24
+ @source_exception = source_exc
25
+ end
26
+ end
27
+ end