jekyll-algolia 1.0.0 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CONTRIBUTING.md +51 -30
  3. data/README.md +69 -27
  4. data/lib/errors/invalid_credentials.txt +12 -0
  5. data/lib/errors/invalid_index_name.txt +9 -0
  6. data/lib/errors/missing_api_key.txt +15 -0
  7. data/lib/errors/missing_application_id.txt +11 -0
  8. data/lib/errors/missing_index_name.txt +18 -0
  9. data/lib/errors/no_records_found.txt +14 -0
  10. data/lib/errors/record_too_big.txt +27 -0
  11. data/lib/errors/record_too_big_api.txt +10 -0
  12. data/lib/errors/settings_manually_edited.txt +17 -0
  13. data/lib/errors/too_many_records.txt +14 -0
  14. data/lib/errors/unknown_application_id.txt +16 -0
  15. data/lib/errors/unknown_settings.txt +12 -0
  16. data/lib/jekyll-algolia.rb +45 -60
  17. data/lib/jekyll/algolia/configurator.rb +137 -44
  18. data/lib/jekyll/algolia/error_handler.rb +36 -48
  19. data/lib/jekyll/algolia/extractor.rb +16 -6
  20. data/lib/jekyll/algolia/file_browser.rb +161 -68
  21. data/lib/jekyll/algolia/hooks.rb +18 -6
  22. data/lib/jekyll/algolia/indexer.rb +283 -145
  23. data/lib/jekyll/algolia/logger.rb +39 -8
  24. data/lib/jekyll/algolia/overwrites/githubpages-configuration.rb +32 -0
  25. data/lib/jekyll/algolia/overwrites/jekyll-algolia-site.rb +151 -0
  26. data/lib/jekyll/algolia/overwrites/jekyll-document.rb +13 -0
  27. data/lib/jekyll/algolia/overwrites/jekyll-paginate-pager.rb +20 -0
  28. data/lib/jekyll/algolia/overwrites/jekyll-tags-link.rb +33 -0
  29. data/lib/jekyll/algolia/progress_bar.rb +27 -0
  30. data/lib/jekyll/algolia/shrinker.rb +112 -0
  31. data/lib/jekyll/algolia/utils.rb +118 -2
  32. data/lib/jekyll/algolia/version.rb +1 -1
  33. data/lib/jekyll/commands/algolia.rb +3 -14
  34. metadata +75 -31
  35. data/errors/invalid_credentials.txt +0 -10
  36. data/errors/invalid_credentials_for_tmp_index.txt +0 -17
  37. data/errors/invalid_index_name.txt +0 -11
  38. data/errors/missing_api_key.txt +0 -17
  39. data/errors/missing_application_id.txt +0 -12
  40. data/errors/missing_index_name.txt +0 -19
  41. data/errors/no_records_found.txt +0 -20
  42. data/errors/record_too_big.txt +0 -25
  43. data/errors/unknown_application_id.txt +0 -20
  44. data/errors/unknown_settings.txt +0 -15
@@ -0,0 +1,12 @@
1
+ E:[✗ Error] Unknown setting
2
+ E:
3
+ E:The jekyll-algolia plugin could not correctly configure your index as some of the settings passed are not recognized.
4
+ W:
5
+ W:It seems that one of the custom index settings you defined was not recognized by the API and was rejected:
6
+ W:  {setting_name}: {setting_value}
7
+ I:
8
+ I:Make sure the setting name and value are correct. You can find a list of all the available settings with their documentation in our API reference:
9
+ I:  https://www.algolia.com/doc/api-reference/api-parameters/
10
+ I:Or specifically for this setting:
11
+ I:  https://www.algolia.com/doc/api-reference/api-parameters/{setting_name}/
12
+ I:
@@ -1,22 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'jekyll/commands/algolia'
4
+ require 'date'
4
5
 
5
6
  module Jekyll
6
7
  # Requirable file, loading all dependencies.
7
8
  # Methods here are called by the main `jekyll algolia` command
8
9
  module Algolia
9
- require 'jekyll/algolia/version'
10
- require 'jekyll/algolia/utils'
11
- require 'jekyll/algolia/hooks'
12
10
  require 'jekyll/algolia/configurator'
13
- require 'jekyll/algolia/logger'
14
11
  require 'jekyll/algolia/error_handler'
15
- require 'jekyll/algolia/file_browser'
16
12
  require 'jekyll/algolia/extractor'
13
+ require 'jekyll/algolia/file_browser'
14
+ require 'jekyll/algolia/hooks'
17
15
  require 'jekyll/algolia/indexer'
16
+ require 'jekyll/algolia/logger'
17
+ require 'jekyll/algolia/progress_bar'
18
+ require 'jekyll/algolia/shrinker'
19
+ require 'jekyll/algolia/utils'
20
+ require 'jekyll/algolia/version'
18
21
 
19
- @config = {}
22
+ MissingCredentialsError = Class.new(StandardError)
20
23
 
21
24
  # Public: Init the Algolia module
22
25
  #
@@ -26,14 +29,46 @@ module Jekyll
26
29
  # The gist of the plugin works by instanciating a Jekyll site,
27
30
  # monkey-patching its `write` method and building it.
28
31
  def self.init(config = {})
29
- @config = config
30
- @site = Jekyll::Algolia::Site.new(@config)
32
+ # Monkey patch Jekyll and external plugins
33
+ load_overwrites
34
+
35
+ config = Configurator.init(config).config
36
+ @site = Jekyll::Algolia::Site.new(config)
37
+
38
+ unless Configurator.assert_valid_credentials
39
+ raise(
40
+ MissingCredentialsError,
41
+ "One or more credentials were not found for site at: #{@site.source}"
42
+ )
43
+ end
44
+
45
+ Configurator.warn_of_deprecated_options
31
46
 
32
- exit 1 unless Configurator.assert_valid_credentials
47
+ if Configurator.dry_run?
48
+ Logger.log('W:==== THIS IS A DRY RUN ====')
49
+ Logger.log('W: - No records will be pushed to your index')
50
+ Logger.log('W: - No settings will be updated on your index')
51
+ end
33
52
 
34
53
  self
35
54
  end
36
55
 
56
+ # Public: Monkey patch Jekyll and external plugins so they don't interfere
57
+ # with our plugin
58
+ #
59
+ # Note: This is only loaded when running `jekyll algolia` so should not have
60
+ # any impact on regular builds
61
+ def self.load_overwrites
62
+ require 'jekyll/algolia/overwrites/githubpages-configuration'
63
+ require 'jekyll/algolia/overwrites/jekyll-algolia-site'
64
+ require 'jekyll/algolia/overwrites/jekyll-document'
65
+ require 'jekyll/algolia/overwrites/jekyll-paginate-pager'
66
+ require 'jekyll/algolia/overwrites/jekyll-tags-link'
67
+
68
+ # Register our own tags to overwrite the default tags
69
+ Liquid::Template.register_tag('link', JekyllAlgoliaLink)
70
+ end
71
+
37
72
  # Public: Run the main Algolia module
38
73
  #
39
74
  # Actually "process" the site, which will acts just like a regular `jekyll
@@ -43,65 +78,15 @@ module Jekyll
43
78
  # Note: The internal list of files to be processed will only be created when
44
79
  # calling .process
45
80
  def self.run
81
+ Logger.log('I:Processing site...')
46
82
  @site.process
47
83
  end
48
84
 
49
- # Public: Get access to the Jekyll config
50
- #
51
- # All other classes will need access to this config, so we make it publicly
52
- # accessible
53
- def self.config
54
- @config
55
- end
56
-
57
85
  # Public: Get access to the Jekyll site
58
86
  #
59
87
  # Tests will need access to the inner Jekyll website so we expose it here
60
88
  def self.site
61
89
  @site
62
90
  end
63
-
64
- # A Jekyll::Site subclass that overrides #write from the parent class to
65
- # create JSON records out of rendered documents and push those records
66
- # to Algolia instead of writing files to disk.
67
- class Site < Jekyll::Site
68
- def write
69
- if Configurator.dry_run?
70
- Logger.log('W:==== THIS IS A DRY RUN ====')
71
- Logger.log('W: - No records will be pushed to your index')
72
- Logger.log('W: - No settings will be updated on your index')
73
- end
74
-
75
- records = []
76
- files = []
77
- each_site_file do |file|
78
- # Skip files that should not be indexed
79
- is_indexable = FileBrowser.indexable?(file)
80
- unless is_indexable
81
- Logger.verbose("W:Skipping #{file.path}")
82
- next
83
- end
84
-
85
- path = FileBrowser.path_from_root(file)
86
- Logger.verbose("I:Extracting records from #{path}")
87
- file_records = Extractor.run(file)
88
-
89
- files << file
90
- records += file_records
91
- end
92
-
93
- # Applying the user hook on the whole list of records
94
- records = Hooks.apply_all(records)
95
-
96
- # Adding a unique objectID to each record
97
- records.map! do |record|
98
- Extractor.add_unique_object_id(record)
99
- end
100
-
101
- Logger.verbose("I:Found #{files.length} files")
102
-
103
- Indexer.run(records)
104
- end
105
- end
106
91
  end
107
92
  end
@@ -6,64 +6,93 @@ module Jekyll
6
6
  module Configurator
7
7
  include Jekyll::Algolia
8
8
 
9
+ @config = {}
10
+
9
11
  # Algolia default values
10
12
  ALGOLIA_DEFAULTS = {
11
13
  'extensions_to_index' => nil,
12
14
  'files_to_exclude' => nil,
13
15
  'nodes_to_index' => 'p',
14
16
  'indexing_batch_size' => 1000,
15
- 'indexing_mode' => 'diff',
17
+ 'max_record_size' => 10_000,
16
18
  'settings' => {
17
- 'distinct' => true,
18
- 'attributeForDistinct' => 'url',
19
- 'attributesForFaceting' => %w[
20
- searchable(tags)
21
- searchable(type)
22
- searchable(title)
19
+ # Searchable attributes
20
+ 'searchableAttributes' => %w[
21
+ title
22
+ headings
23
+ unordered(content)
24
+ collection,categories,tags
23
25
  ],
26
+ # Custom Ranking
24
27
  'customRanking' => [
25
28
  'desc(date)',
26
- 'desc(weight.heading)',
27
- 'asc(weight.position)'
29
+ 'desc(custom_ranking.heading)',
30
+ 'asc(custom_ranking.position)'
28
31
  ],
29
- 'highlightPreTag' => '<em class="ais-Highlight">',
30
- 'highlightPostTag' => '</em>',
31
- 'searchableAttributes' => %w[
32
- title
33
- hierarchy.lvl0
34
- hierarchy.lvl1
35
- hierarchy.lvl2
36
- hierarchy.lvl3
37
- hierarchy.lvl4
38
- hierarchy.lvl5
39
- unordered(content)
40
- collection,unordered(categories),unordered(tags)
32
+ 'unretrievableAttributes' => [
33
+ 'custom_ranking'
41
34
  ],
42
- # We want to allow highlight in more keys than what we search on
35
+ # Highlight
43
36
  'attributesToHighlight' => %w[
44
37
  title
45
- hierarchy.lvl0
46
- hierarchy.lvl1
47
- hierarchy.lvl2
48
- hierarchy.lvl3
49
- hierarchy.lvl4
50
- hierarchy.lvl5
38
+ headings
51
39
  content
52
40
  html
53
41
  collection
54
42
  categories
55
43
  tags
44
+ ],
45
+ 'highlightPreTag' => '<em class="ais-Highlight">',
46
+ 'highlightPostTag' => '</em>',
47
+ # Snippet
48
+ 'attributesToSnippet' => %w[
49
+ content:55
50
+ ],
51
+ 'snippetEllipsisText' => '…',
52
+ # Distinct
53
+ 'distinct' => true,
54
+ 'attributeForDistinct' => 'url',
55
+ # Faceting
56
+ 'attributesForFaceting' => %w[
57
+ type
58
+ searchable(collection)
59
+ searchable(categories)
60
+ searchable(tags)
61
+ searchable(title)
56
62
  ]
57
63
  }
58
64
  }.freeze
59
65
 
66
+ # Public: Init the configurator with the Jekyll config
67
+ #
68
+ # config - The config passed by the `jekyll algolia` command. Default to
69
+ # the default Jekyll config
70
+ def self.init(config = nil)
71
+ # Use the default Jekyll configuration if none specified. Silence the
72
+ # warning about no config set
73
+ Logger.silent { config = Jekyll.configuration } if config.nil?
74
+
75
+ @config = config
76
+
77
+ @config = disable_other_plugins(@config)
78
+
79
+ self
80
+ end
81
+
82
+ # Public: Access to the global configuration object
83
+ #
84
+ # This is a method around @config so we can mock it in the tests
85
+ def self.config
86
+ @config
87
+ end
88
+
60
89
  # Public: Get the value of a specific Jekyll configuration option
61
90
  #
62
91
  # key - Key to read
63
92
  #
64
93
  # Returns the value of this configuration option, nil otherwise
65
94
  def self.get(key)
66
- Jekyll::Algolia.config[key]
95
+ config[key]
67
96
  end
68
97
 
69
98
  # Public: Get the value of a specific Algolia configuration option, or
@@ -74,7 +103,7 @@ module Jekyll
74
103
  # Returns the value of this option, or the default value
75
104
  def self.algolia(key)
76
105
  config = get('algolia') || {}
77
- value = config[key] || ALGOLIA_DEFAULTS[key]
106
+ value = config[key].nil? ? ALGOLIA_DEFAULTS[key] : config[key]
78
107
 
79
108
  # No value found but we have a method to define the default value
80
109
  if value.nil? && respond_to?("default_#{key}")
@@ -120,27 +149,22 @@ module Jekyll
120
149
  ENV['ALGOLIA_INDEX_NAME'] || algolia('index_name')
121
150
  end
122
151
 
152
+ # Public: Return the name of the index used to store the object ids
153
+ def self.index_object_ids_name
154
+ "#{index_name}_object_ids"
155
+ end
156
+
123
157
  # Public: Get the index settings
124
158
  #
125
159
  # This will be a merge of default settings and the one defined in the
126
160
  # _config.yml file
127
161
  def self.settings
162
+ return {} if algolia('settings') == false
163
+
128
164
  user_settings = algolia('settings') || {}
129
165
  ALGOLIA_DEFAULTS['settings'].merge(user_settings)
130
166
  end
131
167
 
132
- # Public: Return the current indexing mode
133
- #
134
- # Default mode is `diff`, but users can configure their own by updating
135
- # the `indexing_mode` config in _config.yml. The only other authorized
136
- # value is `atomic`. If an unrecognized mode is defined, it defaults to
137
- # `diff`.
138
- def self.indexing_mode
139
- mode = algolia('indexing_mode') || ALGOLIA_DEFAULTS['indexing_mode']
140
- return 'diff' unless %w[diff atomic].include?(mode)
141
- mode
142
- end
143
-
144
168
  # Public: Check that all credentials are set
145
169
  #
146
170
  # Returns true if everything is ok, false otherwise. Will display helpful
@@ -162,7 +186,8 @@ module Jekyll
162
186
  # Markdown files can have many different extensions. We keep the one
163
187
  # defined in the Jekyll config
164
188
  def self.default_extensions_to_index
165
- ['html'] + get('markdown_ext').split(',')
189
+ markdown_ext = get('markdown_ext') || ''
190
+ ['html'] + markdown_ext.split(',')
166
191
  end
167
192
 
168
193
  # Public: Setting a default value to ignore index.html/index.md files in
@@ -175,7 +200,7 @@ module Jekyll
175
200
  # User can still add it by manually specifying a `files_to_exclude` to an
176
201
  # empty array
177
202
  def self.default_files_to_exclude
178
- algolia('extensions_to_index').map do |extension|
203
+ extensions_to_index.map do |extension|
179
204
  "index.#{extension}"
180
205
  end
181
206
  end
@@ -186,6 +211,7 @@ module Jekyll
186
211
  def self.verbose?
187
212
  value = get('verbose')
188
213
  return true if value == true
214
+
189
215
  false
190
216
  end
191
217
 
@@ -195,8 +221,75 @@ module Jekyll
195
221
  def self.dry_run?
196
222
  value = get('dry_run')
197
223
  return true if value == true
224
+
225
+ false
226
+ end
227
+
228
+ # Public: Returns true if the command should always update the settings
229
+ #
230
+ # When set to true, the index settings will always be updated, no matter
231
+ # if they've been modified or not
232
+ def self.force_settings?
233
+ value = get('force_settings')
234
+ return true if value == true
235
+
198
236
  false
199
237
  end
238
+
239
+ # Public: Returns a list of extensions to index
240
+ #
241
+ # Will use default values or read the algolia.extensions_to_index key.
242
+ # Accepts both an array or a comma-separated list
243
+ def self.extensions_to_index
244
+ extensions = algolia('extensions_to_index')
245
+ return [] if extensions.nil?
246
+
247
+ extensions = extensions.split(',') if extensions.is_a? String
248
+ extensions
249
+ end
250
+
251
+ # Public: Disable features from other Jekyll plugins that might interfere
252
+ # with the indexing
253
+ # Note that if other jekyll plugins are defined as part of the
254
+ # :jekyll_plugins group in the Gemfile, we might be able to override them
255
+ # using .load_overwrites in jekyll-algolia.rb.
256
+ # If they are simply required in Gemfile, then we might need to revert
257
+ # their values to nil values from here
258
+ def self.disable_other_plugins(config)
259
+ # Disable archive pages from jekyll-archives
260
+ config.delete('jekyll-archives')
261
+
262
+ # Disable pagination from jekyll-paginate
263
+ config.delete('paginate')
264
+
265
+ # Disable pagination for jekyll-paginate-v2
266
+ config['pagination'] = {} unless config['pagination'].is_a?(Hash)
267
+ config['pagination']['enabled'] = false
268
+
269
+ # Disable autopages for jekyll-paginate-v2
270
+ config['autopages'] = {} unless config['autopages'].is_a?(Hash)
271
+ config['autopages']['enabled'] = false
272
+
273
+ # Disable tags from jekyll-tagging
274
+ config.delete('tag_page_dir')
275
+ config.delete('tag_page_layout')
276
+
277
+ config
278
+ end
279
+
280
+ # Public: Check for any deprecated config option and warn the user
281
+ def self.warn_of_deprecated_options
282
+ # indexing_mode is no longer used
283
+ return if algolia('indexing_mode').nil?
284
+
285
+ # rubocop:disable Metrics/LineLength
286
+ Logger.log('I:')
287
+ Logger.log('W:[jekyll-algolia] You are using the algolia.indexing_mode option which has been deprecated in v1.1')
288
+ Logger.log('I: Indexing is now always using an atomic diff algorithm.')
289
+ Logger.log('I: This option is no longer necessary, you can remove it from your _config.yml')
290
+ Logger.log('I:')
291
+ # rubocop:enable Metrics/LineLength
292
+ end
200
293
  end
201
294
  end
202
295
  end
@@ -18,7 +18,6 @@ module Jekyll
18
18
  # happened to the display
19
19
  def self.stop(error, context = {})
20
20
  Logger.verbose("E:[jekyll-algolia] Raw error: #{error}")
21
- Logger.verbose("E:[jekyll-algolia] Context: #{context}")
22
21
 
23
22
  identified_error = identify(error, context)
24
23
 
@@ -47,10 +46,10 @@ module Jekyll
47
46
  def self.identify(error, context = {})
48
47
  known_errors = %w[
49
48
  unknown_application_id
50
- invalid_credentials_for_tmp_index
51
49
  invalid_credentials
52
- record_too_big
53
- unknown_settings
50
+ record_too_big_api
51
+ too_many_records
52
+ unknown_setting
54
53
  invalid_index_name
55
54
  ]
56
55
 
@@ -58,6 +57,7 @@ module Jekyll
58
57
  known_errors.each do |potential_error|
59
58
  error_check = send("#{potential_error}?", error, context)
60
59
  next if error_check == false
60
+
61
61
  return {
62
62
  name: potential_error,
63
63
  details: error_check
@@ -156,29 +156,6 @@ module Jekyll
156
156
  { 'application_id' => app_id }
157
157
  end
158
158
 
159
- # Public: Check if credentials specifically can't access the _tmp index
160
- #
161
- # _context - Not used
162
- #
163
- # If the error happens on a _tmp folder, it might mean that the key does
164
- # not have access to the _tmp indices and the error message will reflect
165
- # that.
166
- def self.invalid_credentials_for_tmp_index?(error, _context = {})
167
- details = error_hash(error.message)
168
-
169
- index_name_tmp = details['index_name']
170
- if details['message'] != 'Index not allowed with this API key' ||
171
- index_name_tmp !~ /_tmp$/
172
- return false
173
- end
174
-
175
- {
176
- 'application_id' => Configurator.application_id,
177
- 'index_name' => Configurator.index_name,
178
- 'index_name_tmp' => index_name_tmp
179
- }
180
- end
181
-
182
159
  # Public: Check if the credentials are working
183
160
  #
184
161
  # _context - Not used
@@ -186,45 +163,41 @@ module Jekyll
186
163
  # Application ID and API key submitted don't match any credentials known
187
164
  def self.invalid_credentials?(error, _context = {})
188
165
  details = error_hash(error.message)
166
+ return false if details == false
189
167
 
190
168
  if details['message'] != 'Invalid Application-ID or API key'
191
169
  return false
192
170
  end
193
171
 
194
172
  {
195
- 'application_id' => details['application_id']
173
+ 'application_id' => details['application_id'],
174
+ 'index_name' => Configurator.index_name,
175
+ 'index_object_ids_name' => Configurator.index_object_ids_name
196
176
  }
197
177
  end
198
178
 
199
179
  # Public: Check if the sent records are not too big
200
180
  #
201
- # context[:records] - list of records to push
181
+ # context[:records] - list of records sent in the batch
202
182
  #
203
- # Records cannot weight more that 10Kb. If we're getting this error it
204
- # means that one of the records is too big, so we'll try to give
205
- # informations about it so the user can debug it.
206
- def self.record_too_big?(error, context = {})
183
+ # One of the sent record is too big and has been rejected by the API. This
184
+ # should not happen as we proactively check for record size before pushing
185
+ # them. If it still happens it means that the value set in max_record_size
186
+ # is not matching the value in the plan.
187
+ def self.record_too_big_api?(error, _context = {})
207
188
  details = error_hash(error.message)
189
+ return false if details == false
208
190
 
209
191
  message = details['message']
210
192
  return false if message !~ /^Record .* is too big .*/
211
193
 
212
- # Getting the record size
213
- size, = /.*size=(.*) bytes.*/.match(message).captures
214
- size = Filesize.from("#{size} B").pretty
215
- object_id = details['objectID']
216
-
217
- # Getting record details
218
- record = Utils.find_by_key(context[:records], :objectID, object_id)
194
+ record_size, = /.*size=(.*) bytes.*/.match(message).captures
195
+ record_size_readable = Filesize.from("#{record_size}B").to_s('Kb')
196
+ max_record_size = Configurator.algolia('max_record_size')
219
197
 
220
198
  {
221
- 'object_id' => object_id,
222
- 'object_title' => record[:title],
223
- 'object_url' => record[:url],
224
- 'object_hint' => record[:content][0..100],
225
- 'nodes_to_index' => Configurator.algolia('nodes_to_index'),
226
- 'size' => size,
227
- 'size_limit' => '10 Kb'
199
+ 'record_size' => record_size_readable,
200
+ 'max_record_size' => max_record_size
228
201
  }
229
202
  end
230
203
 
@@ -235,8 +208,9 @@ module Jekyll
235
208
  # The API will block any call that tries to update a setting value that is
236
209
  # not available. We'll tell the user which one so they can fix their
237
210
  # issue.
238
- def self.unknown_settings?(error, context = {})
211
+ def self.unknown_setting?(error, context = {})
239
212
  details = error_hash(error.message)
213
+ return false if details == false
240
214
 
241
215
  message = details['message']
242
216
  return false if message !~ /^Invalid object attributes.*/
@@ -257,6 +231,7 @@ module Jekyll
257
231
  # Some characters are forbidden in index names
258
232
  def self.invalid_index_name?(error, _context = {})
259
233
  details = error_hash(error.message)
234
+ return false if details == false
260
235
 
261
236
  message = details['message']
262
237
  return false if message !~ /^indexName is not valid.*/
@@ -265,6 +240,19 @@ module Jekyll
265
240
  'index_name' => Configurator.index_name
266
241
  }
267
242
  end
243
+
244
+ # Public: Check if the application has too many records
245
+ #
246
+ # We're trying to push too many records and it goes over quota
247
+ def self.too_many_records?(error, _context = {})
248
+ details = error_hash(error.message)
249
+ return false if details == false
250
+
251
+ message = details['message']
252
+ return false if message !~ /^Record quota exceeded.*/
253
+
254
+ {}
255
+ end
268
256
  end
269
257
  end
270
258
  end