imapcli 0.0.1

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.
@@ -0,0 +1,226 @@
1
+ module Imapcli
2
+ # Wrapper for Net::IMAP
3
+ class Client
4
+ require 'net/imap'
5
+ require 'filesize'
6
+ require 'descriptive_statistics'
7
+
8
+ attr_accessor :port, :user, :pass
9
+ attr_reader :responses
10
+
11
+ ## Initializs the Client class.
12
+ ##
13
+ ## +server_with_optional_port+ is the server's domain name; the port may be
14
+ ## added following a colon. Default port is 993.
15
+ ## +user+ is the user (account) name to log into the server.
16
+ ## +pass+ is the password to log into the server.
17
+ def initialize(server_with_optional_port, user, pass)
18
+ @port = 993 # default
19
+ self.server, @user, @pass = server_with_optional_port, user, pass
20
+ clear_log
21
+ end
22
+
23
+ # Attribute reader for the server domain name
24
+ def server
25
+ @server
26
+ end
27
+
28
+ # Attribute writer for the server domain name; a port may be appended with
29
+ # a colon.
30
+ #
31
+ # If no port is appended, the default port (993) will be used.
32
+ def server=(server_with_optional_port)
33
+ match = server_with_optional_port.match('^([^:]+):(\d+)$')
34
+ if match
35
+ @server = match[1]
36
+ @port = match[2].to_i
37
+ else
38
+ @server = server_with_optional_port
39
+ end
40
+ end
41
+
42
+ # Perform basic sanity check on server name
43
+ #
44
+ # Note that a propery regex for an FQDN is hard to achieve.
45
+ # See https://stackoverflow.com/a/106223/270712 and elsewhere.
46
+ def server_valid?
47
+ @server.match? '^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$'
48
+ end
49
+
50
+ # Perform *very* basic sanity check on user name
51
+ #
52
+ def user_valid?
53
+ @user&.length > 0
54
+ end
55
+
56
+ # Returns true if both server and user name are valid.
57
+ def valid?
58
+ server_valid? && user_valid?
59
+ end
60
+
61
+ # Clears the server response log
62
+ def clear_log
63
+ @log = []
64
+ end
65
+
66
+ # Returns the last response from the server
67
+ def last_response
68
+ @log.last
69
+ end
70
+
71
+ # Returns a connection to the server.
72
+ #
73
+ # The value is cached.
74
+ def connection
75
+ @connection ||= Net::IMAP.new(@server, @port, true)
76
+ end
77
+
78
+ # Logs in to the server.
79
+ #
80
+ # Returns true if login was successful, false if not (e..g, invalid
81
+ # credentials).
82
+ def login
83
+ raise('no connection to a server') unless connection
84
+ begin
85
+ response_ok? connection.login(@user, @pass)
86
+ rescue Net::IMAP::NoResponseError => error
87
+ log_error error
88
+ end
89
+ end
90
+
91
+ # Logs out of the server.
92
+ def logout
93
+ # access instance variable to avoid creating a new connection
94
+ @connection.logout if @connection
95
+ end
96
+
97
+ # Returns the server's greeting (which may reveal the server software name
98
+ # such as 'Dovecot').
99
+ def greeting
100
+ query_server { connection.greeting.data.text.strip }
101
+ end
102
+
103
+ # Returns the server's capabilities.
104
+ def capability
105
+ @capability ||= query_server { connection.capability }
106
+ end
107
+
108
+ # Returns the character that is used to separate nested mailbox names.
109
+ def separator
110
+ @separator ||= query_server { connection.list('', '')[0].delim }
111
+ end
112
+
113
+ # Returns true if the server supports the IMAP QUOTA extension.
114
+ def supports_quota
115
+ capability.include? 'QUOTA'
116
+ end
117
+
118
+ # If the server +supports_quota+, returns an array containing the current
119
+ # usage (in kiB), the total quota (in kiB), and the percent usage.
120
+ def quota
121
+ if supports_quota
122
+ @quota ||= begin
123
+ info = query_server { @connection.getquotaroot('INBOX')[1] }
124
+ percent = info.quota.to_i > 0 ? info.usage.to_i.fdiv(info.quota.to_i) * 100 : nil
125
+ [ info.usage, info.quota, percent ]
126
+ end
127
+ end
128
+ end
129
+
130
+ # Returns an array of message indexes for a mailbox.
131
+ #
132
+ # The value is currently NOT cached.
133
+ def messages(mailbox)
134
+ query_server { connection.examine(mailbox) }
135
+ query_server { connection.search('ALL') }
136
+ end
137
+
138
+ # Examines a mailbox and returns statistics about the messages in it.
139
+ #
140
+ # Returns an array with the following keys:
141
+ # * :count: Total count of messages.
142
+ # * :size: Total size of all messages in bytes.
143
+ # * :min: Size of the smallest message.
144
+ # * :q1: First quartile of message sizes.
145
+ # * :median: Median of message sizes.
146
+ # * :q3: Third quartile of messages sizes.
147
+ # * :max: Size of largest message.
148
+ def examine(mailbox)
149
+ # Could use the EXAMINE command to get the number of messages in a mailbox,
150
+ # but we need to retrieve an array of message indexes anyway (to compute
151
+ # the total mailbox size), so we can save one roundtrip to the server.
152
+ # query_server { connection.examine(mailbox) }
153
+ # total = connection.responses['EXISTS'][0]
154
+ # unseen = query_server { connection.search('UNSEEN') }.length
155
+ messages = messages(mailbox)
156
+ count = messages.length
157
+ sizes = query_server { connection.fetch(messages, 'RFC822.SIZE').map { |f| f.attr['RFC822.SIZE'] }.sort }
158
+ {
159
+ count: count,
160
+ size: convert_bytes(sizes.sum),
161
+ min: convert_bytes(sizes.first),
162
+ q1: convert_bytes(sizes.percentile(25)),
163
+ median: convert_bytes(sizes.median),
164
+ q3: convert_bytes(sizes.percentile(75)),
165
+ max: convert_bytes(sizes.last)
166
+ }
167
+ end
168
+
169
+ # Collects stats for all mailboxes recursively.
170
+ def collect_stats
171
+ mailbox_tree.collect_stats(self)
172
+ end
173
+
174
+ # Gets a list of Net::IMAP::MailboxList items, one for each mailbox.
175
+ #
176
+ # The value is cached.
177
+ def mailboxes
178
+ @mailboxes ||= query_server { @connection.list('', '*') }
179
+ end
180
+
181
+ # Returns a tree of +Imapcli::Mailbox+ objects.
182
+ #
183
+ # The value is cached.
184
+ def mailbox_tree
185
+ @mailbox_tree ||= Mailbox.new(mailboxes)
186
+ end
187
+
188
+ # Attempts to locate a given +mailbox+ in the +mailbox_tree+.
189
+ #
190
+ # Returns nil if the mailbox is not found.
191
+ def find_mailbox(mailbox)
192
+ mailbox_tree.find_sub_mailbox(mailbox, separator)
193
+ end
194
+
195
+ private
196
+
197
+ def response_ok?(response)
198
+ @log << response
199
+ response.name == 'OK'
200
+ end
201
+
202
+ def log_error(error)
203
+ @log << error
204
+ false
205
+ end
206
+
207
+ # Wrapper function that can be used to execute code that queries the server.
208
+ #
209
+ # This function ensures that there is a valid +connection+ and raises an
210
+ # error if not. The code that queries the server must be contained in the
211
+ # +block+, and the +block+'s return value is returned by this function.
212
+ # The +connection+'s responses are logged.
213
+ def query_server(&block)
214
+ raise('no connection to a server') unless connection
215
+ result = yield
216
+ @log << connection.responses
217
+ result
218
+ end
219
+
220
+ # Converts a number of bytes to kiB.
221
+ def convert_bytes(bytes)
222
+ bytes.fdiv(1024).round
223
+ end
224
+
225
+ end # class Client
226
+ end # module Imapcli
@@ -0,0 +1,100 @@
1
+ module Imapcli
2
+ # Provides entry points for Imapcli.
3
+ #
4
+ # Most of the methods in this class return
5
+ class Command
6
+ def initialize(client)
7
+ raise 'Imapcli::Client is required' unless client
8
+ @client = client
9
+ end
10
+
11
+ # Checks if the server accepts the login with the given credentials.
12
+ #
13
+ # Returns true if successful, false if not.
14
+ def check
15
+ @client.login
16
+ end
17
+
18
+ # Collects basic information about the server.
19
+ #
20
+ # The block is called repeatedly with informative messages.
21
+ # If login is not successful, an error will be raised.
22
+ def info
23
+ perform do |output|
24
+ output << "greeting: #{@client.greeting}"
25
+ output << "capability: #{@client.capability.join(' ')}"
26
+ output << "hierarchy separator: #{@client.separator}"
27
+ if @client.supports_quota
28
+ usage = Filesize.from(@client.quota[0] + ' kB').pretty
29
+ available = Filesize.from(@client.quota[1] + ' kB').pretty
30
+ output << "quota: #{usage} used, #{available} available (#{@client.quota[2].round(1)}%)"
31
+ else
32
+ output << "quota: IMAP QUOTA extension not supported by this server"
33
+ end
34
+ end
35
+ end
36
+
37
+ def list
38
+ perform do |output|
39
+ @client.collect_stats
40
+ output << "mailboxes (folders) tree:"
41
+ output += traverse_mailbox_tree @client.mailbox_tree, 0
42
+ end
43
+ end
44
+
45
+ def stats(mailboxes)
46
+ perform do |output|
47
+ mailboxes.each do |name|
48
+ if mailbox = @client.find_mailbox(name)
49
+ mailbox.collect_stats(@client)
50
+ output << [
51
+ mailbox.full_name,
52
+ mailbox.stats[:count],
53
+ format_kib(mailbox.stats[:size]),
54
+ format_kib(mailbox.stats[:min]),
55
+ format_kib(mailbox.stats[:q1]),
56
+ format_kib(mailbox.stats[:median]),
57
+ format_kib(mailbox.stats[:q3]),
58
+ format_kib(mailbox.stats[:max])
59
+ ]
60
+ else
61
+ output << [ self.class.unknown_mailbox_prefix + name ] + Array.new(7, '---')
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ def self.unknown_mailbox_prefix
68
+ '!!! '
69
+ end
70
+
71
+ private
72
+
73
+ def perform
74
+ output = []
75
+ if @client.login
76
+ yield output
77
+ else
78
+ raise 'unable to log into server'
79
+ end
80
+ output
81
+ end
82
+
83
+ def traverse_mailbox_tree(mailbox, depth = 0)
84
+ output = []
85
+ if mailbox.has_children?
86
+ indent = (' ' * depth) || ''
87
+ mailbox.children.each do |child|
88
+ output << indent + '- ' + child.name
89
+ output += traverse_mailbox_tree child, depth + 1
90
+ end
91
+ end
92
+ output
93
+ end
94
+
95
+ def format_kib(kib)
96
+ kib.to_s.reverse.gsub(/...(?=.)/,'\&,').reverse + ' kiB'.freeze
97
+ end
98
+
99
+ end
100
+ end
@@ -0,0 +1,108 @@
1
+ module Imapcli
2
+ # In IMAP speak, a mailbox is what one would commonly call a 'folder'
3
+ class Mailbox
4
+ attr_reader :level, :children, :imap_mailbox_list, :name, :stats
5
+
6
+ # Creates a new root Mailbox object and optionally adds sub mailboxes from
7
+ # an array of Net::IMAP::MailboxList items.
8
+ def initialize(mailbox_list_items = nil)
9
+ @level = 0
10
+ @children = {}
11
+ add_mailbox_list(mailbox_list_items) if mailbox_list_items
12
+ end
13
+
14
+ def [](mailbox)
15
+ @children[mailbox]
16
+ end
17
+
18
+ def full_name
19
+ imap_mailbox_list&.name
20
+ end
21
+
22
+ def has_children?
23
+ @children.length > 0
24
+ end
25
+
26
+ def children
27
+ @children.values
28
+ end
29
+
30
+ # Add a list of mailboxes as returned by Net::IMAP#list.
31
+ def add_mailbox_list(array_of_mailbox_list_items)
32
+ array_of_mailbox_list_items.sort_by { |m| m.name.downcase }.each { |i| add_mailbox i }
33
+ end
34
+
35
+ # Adds a sub mailbox designated by the +name+ of a Net::IMAP::MailboxList.
36
+ def add_mailbox(imap_mailbox_list, options = {})
37
+ return unless imap_mailbox_list&.name&.length > 0
38
+ recursive_add(0, imap_mailbox_list, imap_mailbox_list.name, options)
39
+ end
40
+
41
+ # Attempts to locate and retrieve a sub mailbox.
42
+ #
43
+ # Returns nil of none exists with the given name.
44
+ # Name must be relative to the current mailbox.
45
+ def find_sub_mailbox(relative_name, delimiter)
46
+ if relative_name
47
+ sub_mailbox_name, subs_subs = relative_name.split(delimiter, 2)
48
+ if sub_mailbox = @children[sub_mailbox_name]
49
+ sub_mailbox.find_sub_mailbox(subs_subs, delimiter)
50
+ else
51
+ nil # no matching sub mailbox found, stop searching the tree
52
+ end
53
+ else
54
+ self
55
+ end
56
+ end
57
+
58
+ # Collects statistics for this mailbox.
59
+ #
60
+ # +connection+ must be a Net::IMAP object
61
+ def collect_stats(client)
62
+ if full_name # proceed only if this is a mailbox of its own
63
+ @stats = client.examine(full_name)
64
+ end
65
+ end
66
+
67
+ # Collects statistics for this mailbox and all of its children.
68
+ #
69
+ # +connection+ must be a Net::IMAP object
70
+ def collect_stats_recursively(connection)
71
+ collect_stats(connection)
72
+ @children.values.each { |child| child.collect_stats_recursively(connection) }
73
+ end
74
+
75
+ protected
76
+
77
+ def level=(level)
78
+ @level = level
79
+ end
80
+
81
+ def name=(name)
82
+ @name = name
83
+ end
84
+
85
+ def recursive_add(level, imap_mailbox_list, relative_name = nil, options = {})
86
+ delimiter = options[:delimiter] || imap_mailbox_list.delim
87
+ if relative_name
88
+ sub_mailbox_name, subs_subs = relative_name.split(delimiter, 2)
89
+ if options[:case_insensitive] || (level == 0 && relative_name.upcase == 'INBOX')
90
+ key = sub_mailbox_name.upcase
91
+ else
92
+ key = sub_mailbox_name
93
+ end
94
+ # Create a new mailbox if there does not exist one by the name
95
+ unless sub_mailbox = @children[key]
96
+ sub_mailbox = Mailbox.new
97
+ sub_mailbox.level = level
98
+ sub_mailbox.name = sub_mailbox_name
99
+ @children[key] = sub_mailbox
100
+ end
101
+ sub_mailbox.recursive_add(level + 1, imap_mailbox_list, subs_subs, options)
102
+ else # no more sub mailboxes: we've reached the last of the children
103
+ @imap_mailbox_list = imap_mailbox_list
104
+ self
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,3 @@
1
+ module Imapcli
2
+ VERSION = '0.0.1'
3
+ end
data/lib/imapcli.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'imapcli/version.rb'
2
+ require 'imapcli/command.rb'
3
+ require 'imapcli/client.rb'
4
+ require 'imapcli/mailbox.rb'
@@ -0,0 +1,70 @@
1
+ require 'imapcli'
2
+ require 'dotenv'
3
+
4
+ RSpec.describe Imapcli::Client do
5
+
6
+ context 'with mock credentials for a nonexistent server' do
7
+ let(:client) { Imapcli::Client.new('imap.example.com', 'username', 'password') }
8
+ it 'knows when a server name is invalid' do
9
+ client.server = 'i n v a l i d'
10
+ expect(client.server_valid?).to eq false
11
+ end
12
+ it 'knows when a server name is valid' do
13
+ client.server = 'imap.gmail.com'
14
+ expect(client.server_valid?).to eq true
15
+ end
16
+ it 'knows when a user name is invalid' do
17
+ client.user = ''
18
+ expect(client.user_valid?).to eq false
19
+ end
20
+ it 'knows when a user name is valid' do
21
+ client.user = 'bovender@example.com'
22
+ expect(client.user_valid?).to eq true
23
+ end
24
+ it 'uses port 993 by default' do
25
+ expect(client.port).to eq 993
26
+ end
27
+ it 'extracts a port from the server info' do
28
+ client.server = 'imap.example.com:143'
29
+ expect(client.port).to eq 143
30
+ end
31
+ it 'extracts a server from the server string when a port is appended' do
32
+ client.server = 'imap.example.com:143'
33
+ expect(client.server).to eq 'imap.example.com'
34
+ end
35
+ end
36
+
37
+ context 'with valid credentials for an actual server' do
38
+ before :all do
39
+ Dotenv.load
40
+ end
41
+
42
+ let(:client) { Imapcli::Client.new(ENV['IMAP_SERVER'], ENV['IMAP_USER'], ENV['IMAP_PASS']) }
43
+
44
+ it 'the IMAP_SERVER variable must be set' do
45
+ expect(ENV['IMAP_SERVER']).to_not eq(nil)
46
+ end
47
+ it 'the IMAP_USER variable must be set' do
48
+ expect(ENV['IMAP_USER']).to_not eq(nil)
49
+ end
50
+ it 'the IMAP_PASS variable must be set' do
51
+ expect(ENV['IMAP_PASS']).to_not eq(nil)
52
+ end
53
+ it 'successfully logs in to the server' do
54
+ expect(client.login).to eq true
55
+ end
56
+ end
57
+
58
+ context 'with invalid credentials for an actual server' do
59
+ before :all do
60
+ Dotenv.load
61
+ end
62
+
63
+ let(:client) { Imapcli::Client.new(ENV['IMAP_SERVER'], ENV['IMAP_USER'], ENV['IMAP_PASS'] + 'invalid') }
64
+
65
+ it 'cannot log in to the server' do
66
+ expect(client.login).to eq false
67
+ end
68
+ end
69
+
70
+ end
@@ -0,0 +1,21 @@
1
+ require 'imapcli'
2
+ require 'net/imap'
3
+
4
+ RSpec.describe Imapcli::Mailbox do
5
+ # it 'parses a mailbox list' do
6
+ # mailbox_list = Net::IMAP::MailboxList.new(attr: nil, delim: '/', name: 'Root/Subfolder')
7
+ # mailbox_tree = Imapcli::MailboxTree.new(mailbox_list)
8
+ # expect(mailbox_tree.tree.length).to eq 1
9
+ # end
10
+ it 'returns nil if a given sub mailbox does not exist' do
11
+ mailbox = Imapcli::Mailbox.new
12
+ expect(mailbox.find_sub_mailbox('INBOX.does.not.exist', '.')).to eq nil
13
+ end
14
+ it 'adds and retrieves an existing sub mailbox' do
15
+ name = 'Root/Subfolder/Subsubfolder'
16
+ imap_mailbox_list = Net::IMAP::MailboxList.new(nil, '/', name)
17
+ mailbox = Imapcli::Mailbox.new
18
+ mailbox.add_mailbox(imap_mailbox_list)
19
+ expect(mailbox.find_sub_mailbox(name, '/').imap_mailbox_list).to eq imap_mailbox_list
20
+ end
21
+ end
@@ -0,0 +1,102 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # The generated `.rspec` file contains `--require spec_helper` which will cause
4
+ # this file to always be loaded, without a need to explicitly require it in any
5
+ # files.
6
+ #
7
+ # Given that it is always loaded, you are encouraged to keep this file as
8
+ # light-weight as possible. Requiring heavyweight dependencies from this file
9
+ # will add to the boot time of your test suite on EVERY test run, even for an
10
+ # individual file that may not need all of that loaded. Instead, consider making
11
+ # a separate helper file that requires the additional dependencies and performs
12
+ # the additional setup, and require it from the spec files that actually need
13
+ # it.
14
+ #
15
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
16
+ require 'pry'
17
+
18
+ RSpec.configure do |config|
19
+ # rspec-expectations config goes here. You can use an alternate
20
+ # assertion/expectation library such as wrong or the stdlib/minitest
21
+ # assertions if you prefer.
22
+ config.expect_with :rspec do |expectations|
23
+ # This option will default to `true` in RSpec 4. It makes the `description`
24
+ # and `failure_message` of custom matchers include text for helper methods
25
+ # defined using `chain`, e.g.:
26
+ # be_bigger_than(2).and_smaller_than(4).description
27
+ # # => "be bigger than 2 and smaller than 4"
28
+ # ...rather than:
29
+ # # => "be bigger than 2"
30
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
31
+ end
32
+
33
+ # rspec-mocks config goes here. You can use an alternate test double
34
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
35
+ config.mock_with :rspec do |mocks|
36
+ # Prevents you from mocking or stubbing a method that does not exist on
37
+ # a real object. This is generally recommended, and will default to
38
+ # `true` in RSpec 4.
39
+ mocks.verify_partial_doubles = true
40
+ end
41
+
42
+ # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
43
+ # have no way to turn it off -- the option exists only for backwards
44
+ # compatibility in RSpec 3). It causes shared context metadata to be
45
+ # inherited by the metadata hash of host groups and examples, rather than
46
+ # triggering implicit auto-inclusion in groups with matching metadata.
47
+ config.shared_context_metadata_behavior = :apply_to_host_groups
48
+
49
+ # The settings below are suggested to provide a good initial experience
50
+ # with RSpec, but feel free to customize to your heart's content.
51
+ =begin
52
+ # This allows you to limit a spec run to individual examples or groups
53
+ # you care about by tagging them with `:focus` metadata. When nothing
54
+ # is tagged with `:focus`, all examples get run. RSpec also provides
55
+ # aliases for `it`, `describe`, and `context` that include `:focus`
56
+ # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
57
+ config.filter_run_when_matching :focus
58
+
59
+ # Allows RSpec to persist some state between runs in order to support
60
+ # the `--only-failures` and `--next-failure` CLI options. We recommend
61
+ # you configure your source control system to ignore this file.
62
+ config.example_status_persistence_file_path = "spec/examples.txt"
63
+
64
+ # Limits the available syntax to the non-monkey patched syntax that is
65
+ # recommended. For more details, see:
66
+ # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
67
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
68
+ # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
69
+ config.disable_monkey_patching!
70
+
71
+ # This setting enables warnings. It's recommended, but in some cases may
72
+ # be too noisy due to issues in dependencies.
73
+ config.warnings = true
74
+
75
+ # Many RSpec users commonly either run the entire suite or an individual
76
+ # file, and it's useful to allow more verbose output when running an
77
+ # individual spec file.
78
+ if config.files_to_run.one?
79
+ # Use the documentation formatter for detailed output,
80
+ # unless a formatter has already been configured
81
+ # (e.g. via a command-line flag).
82
+ config.default_formatter = "doc"
83
+ end
84
+
85
+ # Print the 10 slowest examples and example groups at the
86
+ # end of the spec run, to help surface which specs are running
87
+ # particularly slow.
88
+ config.profile_examples = 10
89
+
90
+ # Run specs in random order to surface order dependencies. If you find an
91
+ # order dependency and want to debug it, you can fix the order by providing
92
+ # the seed, which is printed after each run.
93
+ # --seed 1234
94
+ config.order = :random
95
+
96
+ # Seed global randomization in this process using the `--seed` CLI option.
97
+ # Setting this allows you to use `--seed` to deterministically reproduce
98
+ # test failures related to randomization by passing the same `--seed` value
99
+ # as the one that triggered the failure.
100
+ Kernel.srand config.seed
101
+ =end
102
+ end