mail_daemon 0.1.0 → 1.0.0

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,144 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe MailDaemon::Helpers do
4
+ let(:test_class) do
5
+ Class.new do
6
+ include MailDaemon::Helpers
7
+ end
8
+ end
9
+
10
+ let(:instance) { test_class.new }
11
+
12
+ describe '#setup_options' do
13
+ it 'converts string keys to symbols' do
14
+ options = { 'key1' => 'value1', 'key2' => 'value2' }
15
+ instance.setup_options(options)
16
+
17
+ expect(instance.instance_variable_get(:@options)).to eq(
18
+ key1: 'value1',
19
+ key2: 'value2'
20
+ )
21
+ end
22
+
23
+ it 'preserves symbol keys' do
24
+ options = { key1: 'value1', key2: 'value2' }
25
+ instance.setup_options(options)
26
+
27
+ expect(instance.instance_variable_get(:@options)).to eq(
28
+ key1: 'value1',
29
+ key2: 'value2'
30
+ )
31
+ end
32
+
33
+ it 'handles mixed string and symbol keys' do
34
+ options = { 'string_key' => 'value1', symbol_key: 'value2' }
35
+ instance.setup_options(options)
36
+
37
+ expect(instance.instance_variable_get(:@options)).to include(
38
+ string_key: 'value1',
39
+ symbol_key: 'value2'
40
+ )
41
+ end
42
+
43
+ it 'handles empty hash' do
44
+ instance.setup_options({})
45
+ expect(instance.instance_variable_get(:@options)).to eq({})
46
+ end
47
+ end
48
+
49
+ describe '#reload_options' do
50
+ it 'merges new options with existing options' do
51
+ instance.setup_options({ key1: 'value1' })
52
+ instance.reload_options({ key2: 'value2' })
53
+
54
+ expect(instance.instance_variable_get(:@options)).to eq(
55
+ key1: 'value1',
56
+ key2: 'value2'
57
+ )
58
+ end
59
+
60
+ it 'overwrites existing keys' do
61
+ instance.setup_options({ key1: 'old_value' })
62
+ instance.reload_options({ key1: 'new_value' })
63
+
64
+ expect(instance.instance_variable_get(:@options)[:key1]).to eq('new_value')
65
+ end
66
+
67
+ it 'handles string keys in reload' do
68
+ instance.setup_options({ key1: 'value1' })
69
+ instance.reload_options({ 'key2' => 'value2' })
70
+
71
+ expect(instance.instance_variable_get(:@options)).to include(
72
+ key1: 'value1',
73
+ key2: 'value2'
74
+ )
75
+ end
76
+ end
77
+
78
+ describe '#default_option' do
79
+ it 'sets default value when option does not exist' do
80
+ instance.setup_options({})
81
+ instance.default_option(:new_key, 'default_value')
82
+
83
+ expect(instance.instance_variable_get(:@options)[:new_key]).to eq('default_value')
84
+ end
85
+
86
+ it 'does not overwrite existing option' do
87
+ instance.setup_options({ existing_key: 'existing_value' })
88
+ instance.default_option(:existing_key, 'default_value')
89
+
90
+ expect(instance.instance_variable_get(:@options)[:existing_key]).to eq('existing_value')
91
+ end
92
+
93
+ it 'handles string name' do
94
+ instance.setup_options({})
95
+ instance.default_option('string_key', 'value')
96
+
97
+ expect(instance.instance_variable_get(:@options)[:string_key]).to eq('value')
98
+ end
99
+
100
+ it 'handles symbol name' do
101
+ instance.setup_options({})
102
+ instance.default_option(:symbol_key, 'value')
103
+
104
+ expect(instance.instance_variable_get(:@options)[:symbol_key]).to eq('value')
105
+ end
106
+ end
107
+
108
+ describe '#required_option' do
109
+ it 'does not raise when required option exists' do
110
+ instance.setup_options({ required_key: 'value' })
111
+ expect { instance.required_option(:required_key) }.not_to raise_error
112
+ end
113
+
114
+ it 'raises error when required option is missing' do
115
+ instance.setup_options({})
116
+ expect { instance.required_option(:missing_key) }.to raise_error(/missing_key is a required option/)
117
+ end
118
+
119
+ it 'handles string name' do
120
+ instance.setup_options({ 'string_key' => 'value' })
121
+ expect { instance.required_option('string_key') }.not_to raise_error
122
+ end
123
+
124
+ it 'handles multiple required options' do
125
+ instance.setup_options({ key1: 'value1', key2: 'value2' })
126
+ expect { instance.required_option([:key1, :key2]) }.not_to raise_error
127
+ end
128
+
129
+ it 'raises error when any required option is missing' do
130
+ instance.setup_options({ key1: 'value1' })
131
+ expect { instance.required_option([:key1, :missing_key]) }.to raise_error(/missing_key is a required option/)
132
+ end
133
+
134
+ it 'handles single option as array' do
135
+ instance.setup_options({ key1: 'value1' })
136
+ expect { instance.required_option([:key1]) }.not_to raise_error
137
+ end
138
+
139
+ it 'handles single option as symbol' do
140
+ instance.setup_options({ key1: 'value1' })
141
+ expect { instance.required_option(:key1) }.not_to raise_error
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,306 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe MailDaemon::Imap::Connection do
4
+ let(:options) do
5
+ {
6
+ host: 'imap.example.com',
7
+ username: 'user@example.com',
8
+ password: 'password',
9
+ port: 993,
10
+ ssl: true,
11
+ folder: 'inbox',
12
+ search_command: 'UNSEEN',
13
+ debug: false
14
+ }
15
+ end
16
+
17
+ let(:imap_mock) { instance_double(Net::IMAP) }
18
+ let(:status_block) { proc { |status| } }
19
+
20
+ describe '#initialize' do
21
+ it 'sets up options' do
22
+ connection = described_class.new(options, &status_block)
23
+ expect(connection.instance_variable_get(:@options)).to include(
24
+ host: 'imap.example.com',
25
+ username: 'user@example.com',
26
+ password: 'password'
27
+ )
28
+ end
29
+
30
+ it 'requires host option' do
31
+ expect { described_class.new({ username: 'user', password: 'pass' }, &status_block) }.to raise_error(/host is a required option/)
32
+ end
33
+
34
+ it 'requires username option' do
35
+ expect { described_class.new({ host: 'host', password: 'pass' }, &status_block) }.to raise_error(/username is a required option/)
36
+ end
37
+
38
+ it 'requires password option' do
39
+ expect { described_class.new({ host: 'host', username: 'user' }, &status_block) }.to raise_error(/password is a required option/)
40
+ end
41
+
42
+ it 'defaults port to 143' do
43
+ # Remove port from options to test default
44
+ opts = options.dup
45
+ opts.delete(:port)
46
+ connection = described_class.new(opts, &status_block)
47
+ expect(connection.instance_variable_get(:@options)[:port]).to eq(143)
48
+ end
49
+
50
+ it 'defaults folder to inbox' do
51
+ connection = described_class.new(options, &status_block)
52
+ expect(connection.instance_variable_get(:@options)[:folder]).to eq('inbox')
53
+ end
54
+
55
+ it 'defaults ssl to false' do
56
+ # Remove ssl from options to test default
57
+ opts = options.dup
58
+ opts.delete(:ssl)
59
+ connection = described_class.new(opts, &status_block)
60
+ expect(connection.instance_variable_get(:@options)[:ssl]).to eq(false)
61
+ end
62
+
63
+ it 'defaults sleep_time to 30' do
64
+ connection = described_class.new(options, &status_block)
65
+ expect(connection.instance_variable_get(:@options)[:sleep_time]).to eq(30)
66
+ end
67
+
68
+ it 'sets initial status to INITIALIZING' do
69
+ connection = described_class.new(options, &status_block)
70
+ expect(connection.status[:status]).to eq(MailDaemon::Imap::Statuses::INITIALIZING)
71
+ end
72
+ end
73
+
74
+ describe '#login' do
75
+ before do
76
+ allow(Net::IMAP).to receive(:new).and_return(imap_mock)
77
+ allow(imap_mock).to receive(:capability).and_return(['IDLE', 'IMAP4rev1'])
78
+ allow(imap_mock).to receive(:login)
79
+ allow(imap_mock).to receive(:select)
80
+ allow(imap_mock).to receive(:disconnected?).and_return(false)
81
+ end
82
+
83
+ it 'creates IMAP connection' do
84
+ connection = described_class.new(options, &status_block)
85
+ # ssl_options defaults to false, so ssl: false is expected
86
+ expect(Net::IMAP).to receive(:new).with('imap.example.com', port: 993, ssl: false).and_return(imap_mock)
87
+ connection.login
88
+ end
89
+
90
+ it 'checks for IDLE capability' do
91
+ connection = described_class.new(options, &status_block)
92
+ expect(imap_mock).to receive(:capability)
93
+ connection.login
94
+ end
95
+
96
+ it 'sets idle_available when IDLE is supported' do
97
+ connection = described_class.new(options, &status_block)
98
+ connection.login
99
+ expect(connection.instance_variable_get(:@idle_available)).to be true
100
+ end
101
+
102
+ it 'sets idle_available to false when IDLE is not supported' do
103
+ allow(imap_mock).to receive(:capability).and_return(['IMAP4rev1'])
104
+ connection = described_class.new(options, &status_block)
105
+ connection.login
106
+ expect(connection.instance_variable_get(:@idle_available)).to be false
107
+ end
108
+
109
+ it 'logs in with username and password' do
110
+ connection = described_class.new(options, &status_block)
111
+ expect(imap_mock).to receive(:login).with('user@example.com', 'password')
112
+ connection.login
113
+ end
114
+
115
+ it 'selects the inbox folder' do
116
+ connection = described_class.new(options, &status_block)
117
+ expect(imap_mock).to receive(:select).with('INBOX')
118
+ connection.login
119
+ end
120
+
121
+ it 'sets status to LOGGED_ON after successful login' do
122
+ connection = described_class.new(options, &status_block)
123
+ connection.login
124
+ expect(connection.status[:status]).to eq(MailDaemon::Imap::Statuses::LOGGED_ON)
125
+ end
126
+
127
+ it 'uses SSL options when provided' do
128
+ ssl_options = { verify_mode: OpenSSL::SSL::VERIFY_NONE }
129
+ opts = options.merge(ssl_options: ssl_options)
130
+ connection = described_class.new(opts, &status_block)
131
+ expect(Net::IMAP).to receive(:new).with('imap.example.com', port: 993, ssl: ssl_options).and_return(imap_mock)
132
+ connection.login
133
+ end
134
+ end
135
+
136
+ describe '#wait_for_messages' do
137
+ before do
138
+ allow(Net::IMAP).to receive(:new).and_return(imap_mock)
139
+ allow(imap_mock).to receive(:capability).and_return(['IDLE', 'IMAP4rev1'])
140
+ allow(imap_mock).to receive(:login)
141
+ allow(imap_mock).to receive(:select)
142
+ allow(imap_mock).to receive(:disconnected?).and_return(false)
143
+ end
144
+
145
+ it 'fetches message IDs using search command' do
146
+ connection = described_class.new(options, &status_block)
147
+ connection.login
148
+
149
+ # Set idle_required to false BEFORE calling wait_for_messages to prevent infinite loop
150
+ connection.instance_variable_set(:@idle_required, false)
151
+
152
+ allow(imap_mock).to receive(:uid_search).and_return([1, 2, 3])
153
+ # Mock the structure: envelope[0][:attr]["RFC822"]
154
+ fetch_data = { attr: { 'RFC822' => 'message body' } }
155
+ allow(imap_mock).to receive(:uid_fetch).and_return([fetch_data])
156
+
157
+ # Mock idle to not block - it won't be called since idle_required is false, but just in case
158
+ allow(imap_mock).to receive(:idle) do |&block|
159
+ # Don't call the block to prevent blocking
160
+ end
161
+
162
+ expect(imap_mock).to receive(:uid_search).with('UNSEEN').at_least(:once)
163
+ connection.wait_for_messages { |msg| }
164
+ end
165
+
166
+ it 'yields fetched messages' do
167
+ connection = described_class.new(options, &status_block)
168
+ connection.login
169
+
170
+ # Set idle_required to false BEFORE calling wait_for_messages to prevent infinite loop
171
+ connection.instance_variable_set(:@idle_required, false)
172
+
173
+ message_body = 'email message content'
174
+ allow(imap_mock).to receive(:uid_search).and_return([1])
175
+ # Fix the structure to match what fetch_message expects: envelope[0][:attr]["RFC822"]
176
+ # The code does: envelope[0][:attr]["RFC822"]
177
+ # envelope is the return value of uid_fetch, which is an array
178
+ # envelope[0] is the first element, which should support [:attr] access
179
+ # envelope[0][:attr] should return a hash with "RFC822" key
180
+ fetch_data = { attr: { 'RFC822' => message_body } }
181
+ allow(imap_mock).to receive(:uid_fetch).with(1, ['RFC822']).and_return([fetch_data])
182
+
183
+ # Mock idle to not block (won't be called since idle_required is false)
184
+ allow(imap_mock).to receive(:idle) do |&block|
185
+ # Don't call the block to prevent blocking
186
+ end
187
+
188
+ yielded_message = nil
189
+ connection.wait_for_messages { |msg| yielded_message = msg }
190
+
191
+ expect(yielded_message).to eq(message_body)
192
+ end
193
+
194
+ it 'uses polling mode when IDLE is not available' do
195
+ allow(imap_mock).to receive(:capability).and_return(['IMAP4rev1'])
196
+ connection = described_class.new(options, &status_block)
197
+ connection.login
198
+
199
+ # Set idle_required to false BEFORE calling wait_for_messages to prevent infinite loop
200
+ connection.instance_variable_set(:@idle_required, false)
201
+
202
+ allow(imap_mock).to receive(:uid_search).and_return([])
203
+
204
+ expect(imap_mock).to receive(:uid_search).at_least(:once)
205
+ connection.wait_for_messages { |msg| }
206
+ end
207
+ end
208
+
209
+ describe '#logout' do
210
+ before do
211
+ allow(Net::IMAP).to receive(:new).and_return(imap_mock)
212
+ allow(imap_mock).to receive(:capability).and_return(['IDLE', 'IMAP4rev1'])
213
+ allow(imap_mock).to receive(:login)
214
+ allow(imap_mock).to receive(:select)
215
+ allow(imap_mock).to receive(:disconnected?).and_return(false)
216
+ allow(imap_mock).to receive(:logout)
217
+ end
218
+
219
+ it 'logs out from IMAP server' do
220
+ connection = described_class.new(options, &status_block)
221
+ connection.login
222
+ expect(imap_mock).to receive(:logout)
223
+ connection.logout
224
+ end
225
+
226
+ it 'sets status to LOGGED_OFF' do
227
+ connection = described_class.new(options, &status_block)
228
+ connection.login
229
+ connection.logout
230
+ expect(connection.status[:status]).to eq(MailDaemon::Imap::Statuses::LOGGED_OFF)
231
+ end
232
+
233
+ it 'does not logout if already disconnected' do
234
+ connection = described_class.new(options, &status_block)
235
+ connection.login
236
+ allow(imap_mock).to receive(:disconnected?).and_return(true)
237
+ expect(imap_mock).not_to receive(:logout)
238
+ connection.logout
239
+ end
240
+ end
241
+
242
+ describe '#disconnect' do
243
+ before do
244
+ allow(Net::IMAP).to receive(:new).and_return(imap_mock)
245
+ allow(imap_mock).to receive(:capability).and_return(['IDLE', 'IMAP4rev1'])
246
+ allow(imap_mock).to receive(:login)
247
+ allow(imap_mock).to receive(:select)
248
+ allow(imap_mock).to receive(:disconnected?).and_return(false)
249
+ allow(imap_mock).to receive(:logout)
250
+ allow(imap_mock).to receive(:idle_done)
251
+ allow(imap_mock).to receive(:disconnect)
252
+ end
253
+
254
+ it 'disconnects from IMAP server' do
255
+ connection = described_class.new(options, &status_block)
256
+ connection.login
257
+ expect(imap_mock).to receive(:disconnect)
258
+ connection.disconnect
259
+ end
260
+
261
+ it 'sets status to DISCONNECTED' do
262
+ connection = described_class.new(options, &status_block)
263
+ connection.login
264
+ connection.disconnect
265
+ expect(connection.status[:status]).to eq(MailDaemon::Imap::Statuses::DISCONNECTED)
266
+ end
267
+
268
+ it 'handles errors during disconnect gracefully' do
269
+ connection = described_class.new(options, &status_block)
270
+ connection.login
271
+ allow(imap_mock).to receive(:disconnect).and_raise(StandardError.new('Connection error'))
272
+
273
+ expect { connection.disconnect }.not_to raise_error
274
+ expect(connection.status[:status]).to eq(MailDaemon::Imap::Statuses::DISCONNECTED)
275
+ end
276
+ end
277
+
278
+ describe '#running?' do
279
+ it 'returns false when not connected' do
280
+ connection = described_class.new(options, &status_block)
281
+ # @imap is nil until login is called
282
+ expect(connection.running?).to be false
283
+ end
284
+
285
+ it 'returns true when connected' do
286
+ allow(Net::IMAP).to receive(:new).and_return(imap_mock)
287
+ allow(imap_mock).to receive(:capability).and_return(['IDLE', 'IMAP4rev1'])
288
+ allow(imap_mock).to receive(:login)
289
+ allow(imap_mock).to receive(:select)
290
+ allow(imap_mock).to receive(:disconnected?).and_return(false)
291
+
292
+ connection = described_class.new(options, &status_block)
293
+ connection.login
294
+ expect(connection.running?).to be true
295
+ end
296
+ end
297
+
298
+ describe '#status' do
299
+ it 'returns current status' do
300
+ connection = described_class.new(options, &status_block)
301
+ status = connection.status
302
+ expect(status).to have_key(:status)
303
+ expect(status[:status]).to eq(MailDaemon::Imap::Statuses::INITIALIZING)
304
+ end
305
+ end
306
+ end
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe MailDaemon::Imap::Statuses do
4
+ it 'defines INITIALIZING constant' do
5
+ expect(MailDaemon::Imap::Statuses::INITIALIZING).to eq('initializing')
6
+ end
7
+
8
+ it 'defines CONNECTING constant' do
9
+ expect(MailDaemon::Imap::Statuses::CONNECTING).to eq('connecting')
10
+ end
11
+
12
+ it 'defines CONNECTED constant' do
13
+ expect(MailDaemon::Imap::Statuses::CONNECTED).to eq('connected')
14
+ end
15
+
16
+ it 'defines LOGGING_ON constant' do
17
+ expect(MailDaemon::Imap::Statuses::LOGGING_ON).to eq('logging_on')
18
+ end
19
+
20
+ it 'defines LOGGED_ON constant' do
21
+ expect(MailDaemon::Imap::Statuses::LOGGED_ON).to eq('logged_on')
22
+ end
23
+
24
+ it 'defines LOGGING_OFF constant' do
25
+ expect(MailDaemon::Imap::Statuses::LOGGING_OFF).to eq('logging_off')
26
+ end
27
+
28
+ it 'defines LOGGED_OFF constant' do
29
+ expect(MailDaemon::Imap::Statuses::LOGGED_OFF).to eq('logged_off')
30
+ end
31
+
32
+ it 'defines IDLEING constant' do
33
+ expect(MailDaemon::Imap::Statuses::IDLEING).to eq('ready')
34
+ end
35
+
36
+ it 'defines POLLING constant' do
37
+ expect(MailDaemon::Imap::Statuses::POLLING).to eq('ready')
38
+ end
39
+
40
+ it 'defines DISCONNECTING constant' do
41
+ expect(MailDaemon::Imap::Statuses::DISCONNECTING).to eq('disconnecting')
42
+ end
43
+
44
+ it 'defines DISCONNECTED constant' do
45
+ expect(MailDaemon::Imap::Statuses::DISCONNECTED).to eq('disconnected')
46
+ end
47
+ end
@@ -0,0 +1,128 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe MailDaemon::Imap::Watcher do
4
+ let(:options) do
5
+ {
6
+ host: 'imap.example.com',
7
+ username: 'user@example.com',
8
+ password: 'password',
9
+ account_code: 'test_account',
10
+ debug: false
11
+ }
12
+ end
13
+
14
+ describe '#initialize' do
15
+ it 'sets options' do
16
+ watcher = described_class.new(options)
17
+ expect(watcher.instance_variable_get(:@options)).to eq(options)
18
+ end
19
+ end
20
+
21
+ describe '#start' do
22
+ let(:connection) { instance_double(MailDaemon::Imap::Connection) }
23
+
24
+ it 'creates and logs in to connection' do
25
+ watcher = described_class.new(options)
26
+ # Connection.new receives options and a status notification block (the block passed to start)
27
+ expect(MailDaemon::Imap::Connection).to receive(:new).and_return(connection)
28
+ expect(connection).to receive(:login)
29
+ # Mock wait_for_messages to yield once then return to prevent infinite loop
30
+ # Note: wait_for_messages receives a block that yields messages
31
+ allow(connection).to receive(:wait_for_messages) do |&message_block|
32
+ message_block.call('message content') if message_block
33
+ # Set idle_required to false to exit any loops
34
+ connection.instance_variable_set(:@idle_required, false) if connection.respond_to?(:instance_variable_set)
35
+ end
36
+
37
+ # Run in a thread with timeout to prevent hanging
38
+ thread = Thread.new do
39
+ watcher.start { |msg| }
40
+ end
41
+
42
+ sleep 0.1
43
+ thread.kill if thread.alive?
44
+ thread.join(0.1)
45
+ end
46
+
47
+ it 'yields messages with type and mailbox info' do
48
+ watcher = described_class.new(options)
49
+ allow(MailDaemon::Imap::Connection).to receive(:new).and_return(connection)
50
+ allow(connection).to receive(:login)
51
+ # Mock wait_for_messages to yield once then return
52
+ allow(connection).to receive(:wait_for_messages) do |&block|
53
+ block.call('message content') if block
54
+ connection.instance_variable_set(:@idle_required, false) if connection.respond_to?(:instance_variable_set)
55
+ end
56
+
57
+ yielded_data = nil
58
+
59
+ thread = Thread.new do
60
+ watcher.start do |data|
61
+ yielded_data = data
62
+ end
63
+ end
64
+
65
+ sleep 0.1
66
+ thread.kill if thread.alive?
67
+ thread.join(0.1)
68
+
69
+ expect(yielded_data).to include(
70
+ type: 'incoming_email',
71
+ mailbox: options,
72
+ inbound_message: 'message content'
73
+ )
74
+ end
75
+ end
76
+
77
+ describe '#stop' do
78
+ it 'disconnects the connection' do
79
+ watcher = described_class.new(options)
80
+ connection = instance_double(MailDaemon::Imap::Connection)
81
+ watcher.instance_variable_set(:@connection, connection)
82
+
83
+ expect(connection).to receive(:disconnect)
84
+ watcher.stop
85
+ end
86
+ end
87
+
88
+ describe '#restart' do
89
+ it 'stops and starts the connection' do
90
+ watcher = described_class.new(options)
91
+ connection = instance_double(MailDaemon::Imap::Connection)
92
+ watcher.instance_variable_set(:@connection, connection)
93
+
94
+ expect(watcher).to receive(:stop)
95
+ expect(watcher).to receive(:start)
96
+ watcher.restart
97
+ end
98
+ end
99
+
100
+ describe '#running?' do
101
+ it 'returns false when connection is nil' do
102
+ watcher = described_class.new(options)
103
+ expect(watcher.running?).to be false
104
+ end
105
+
106
+ it 'delegates to connection running? method' do
107
+ watcher = described_class.new(options)
108
+ connection = instance_double(MailDaemon::Imap::Connection, running?: true)
109
+ watcher.instance_variable_set(:@connection, connection)
110
+
111
+ expect(watcher.running?).to be true
112
+ end
113
+ end
114
+
115
+ describe '#mailbox' do
116
+ it 'returns mailbox from options' do
117
+ watcher = described_class.new(options.merge(mailbox: { name: 'inbox' }))
118
+ expect(watcher.mailbox).to eq({ name: 'inbox' })
119
+ end
120
+ end
121
+
122
+ describe '#to_s' do
123
+ it 'returns account_code and username' do
124
+ watcher = described_class.new(options)
125
+ expect(watcher.to_s).to eq('test_account/user@example.com')
126
+ end
127
+ end
128
+ end