larch 1.0.0 → 1.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.
- data/HISTORY +23 -0
- data/README.rdoc +185 -0
- data/bin/larch +91 -34
- data/lib/larch/errors.rb +2 -1
- data/lib/larch/imap/mailbox.rb +279 -0
- data/lib/larch/imap.rb +167 -246
- data/lib/larch/logger.rb +6 -5
- data/lib/larch/version.rb +1 -1
- data/lib/larch.rb +142 -73
- metadata +9 -5
- data/lib/larch/util.rb +0 -17
data/lib/larch/imap.rb
CHANGED
@@ -6,38 +6,29 @@ module Larch
|
|
6
6
|
# required reading if you're doing anything with IMAP in Ruby:
|
7
7
|
# http://sup.rubyforge.org
|
8
8
|
class IMAP
|
9
|
-
|
10
|
-
# Recoverable connection errors.
|
11
|
-
RECOVERABLE_ERRORS = [
|
12
|
-
Errno::EPIPE,
|
13
|
-
Errno::ETIMEDOUT,
|
14
|
-
Net::IMAP::NoResponseError,
|
15
|
-
OpenSSL::SSL::SSLError
|
16
|
-
]
|
17
|
-
|
18
|
-
# Regex to capture the individual fields in an IMAP fetch command.
|
19
|
-
REGEX_FIELDS = /([0-9A-Z\.]+\[[^\]]+\](?:<[0-9\.]+>)?|[0-9A-Z\.]+)/
|
20
|
-
|
21
|
-
# Regex to capture a Message-Id header.
|
22
|
-
REGEX_MESSAGE_ID = /message-id\s*:\s*(\S+)/i
|
9
|
+
attr_reader :conn, :options
|
23
10
|
|
24
11
|
# URI format validation regex.
|
25
12
|
REGEX_URI = URI.regexp(['imap', 'imaps'])
|
26
13
|
|
27
|
-
# Minimum time (in seconds) allowed between mailbox scans.
|
28
|
-
SCAN_INTERVAL = 60
|
29
|
-
|
30
14
|
# Larch::IMAP::Message represents a transferable IMAP message which can be
|
31
15
|
# passed between Larch::IMAP instances.
|
32
16
|
Message = Struct.new(:id, :envelope, :rfc822, :flags, :internaldate)
|
33
17
|
|
34
18
|
# Initializes a new Larch::IMAP instance that will connect to the specified
|
35
|
-
# IMAP URI
|
19
|
+
# IMAP URI.
|
36
20
|
#
|
37
|
-
#
|
21
|
+
# In addition to the URI, the following options may also be specified:
|
38
22
|
#
|
39
23
|
# [:create_mailbox]
|
40
|
-
# If +true+,
|
24
|
+
# If +true+, mailboxes that don't already exist will be created if
|
25
|
+
# necessary.
|
26
|
+
#
|
27
|
+
# [:dry_run]
|
28
|
+
# If +true+, read-only operations will be performed as usual and all change
|
29
|
+
# operations will be simulated, but no changes will actually be made. Note
|
30
|
+
# that it's not actually possible to simulate mailbox creation, so
|
31
|
+
# +:dry_run+ mode always behaves as if +:create_mailbox+ is +false+.
|
41
32
|
#
|
42
33
|
# [:fast_scan]
|
43
34
|
# If +true+, a faster but less accurate method will be used to scan
|
@@ -51,29 +42,36 @@ class IMAP
|
|
51
42
|
# After a recoverable error occurs, retry the operation up to this many
|
52
43
|
# times. Default is 3.
|
53
44
|
#
|
54
|
-
|
45
|
+
# [:ssl_certs]
|
46
|
+
# Path to a trusted certificate bundle to use to verify server SSL
|
47
|
+
# certificates. You can download a bundle of certificate authority root
|
48
|
+
# certs at http://curl.haxx.se/ca/cacert.pem (it's up to you to verify that
|
49
|
+
# this bundle hasn't been tampered with, however; don't trust it blindly).
|
50
|
+
#
|
51
|
+
# [:ssl_verify]
|
52
|
+
# If +true+, server SSL certificates will be verified against the trusted
|
53
|
+
# certificate bundle specified in +ssl_certs+. By default, server SSL
|
54
|
+
# certificates are not verified.
|
55
|
+
#
|
56
|
+
def initialize(uri, options = {})
|
55
57
|
raise ArgumentError, "not an IMAP URI: #{uri}" unless uri.is_a?(URI) || uri =~ REGEX_URI
|
56
|
-
raise ArgumentError, "must provide a username and password" unless username && password
|
57
58
|
raise ArgumentError, "options must be a Hash" unless options.is_a?(Hash)
|
58
59
|
|
59
|
-
@
|
60
|
-
@
|
61
|
-
@password = password
|
62
|
-
@options = {:max_retries => 3}.merge(options)
|
60
|
+
@options = {:max_retries => 3, :ssl_verify => false}.merge(options)
|
61
|
+
@uri = uri.is_a?(URI) ? uri : URI(uri)
|
63
62
|
|
64
|
-
@
|
65
|
-
|
66
|
-
@
|
67
|
-
@
|
68
|
-
@
|
69
|
-
@mutex = Mutex.new
|
63
|
+
raise ArgumentError, "must provide a username and password" unless @uri.user && @uri.password
|
64
|
+
|
65
|
+
@conn = nil
|
66
|
+
@mailboxes = {}
|
67
|
+
@mutex = Mutex.new
|
70
68
|
|
71
69
|
# Create private convenience methods (debug, info, warn, etc.) to make
|
72
70
|
# logging easier.
|
73
71
|
Logger::LEVELS.each_key do |level|
|
74
72
|
IMAP.class_eval do
|
75
73
|
define_method(level) do |msg|
|
76
|
-
Larch.log.log(level, "#{
|
74
|
+
Larch.log.log(level, "#{username}@#{host}: #{msg}")
|
77
75
|
end
|
78
76
|
|
79
77
|
private level
|
@@ -81,120 +79,80 @@ class IMAP
|
|
81
79
|
end
|
82
80
|
end
|
83
81
|
|
84
|
-
# Appends the specified Larch::IMAP::Message to this mailbox if it doesn't
|
85
|
-
# already exist. Returns +true+ if the message was appended successfully,
|
86
|
-
# +false+ if the message already exists in the mailbox.
|
87
|
-
def append(message)
|
88
|
-
raise ArgumentError, "must provide a Larch::IMAP::Message object" unless message.is_a?(Message)
|
89
|
-
return false if has_message?(message)
|
90
|
-
|
91
|
-
safely do
|
92
|
-
@mutex.synchronize do
|
93
|
-
begin
|
94
|
-
@imap.select(mailbox)
|
95
|
-
rescue Net::IMAP::NoResponseError => e
|
96
|
-
if @options[:create_mailbox]
|
97
|
-
info "creating mailbox: #{mailbox}"
|
98
|
-
@imap.create(mailbox)
|
99
|
-
retry
|
100
|
-
end
|
101
|
-
|
102
|
-
raise
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
debug "appending message: #{message.id}"
|
107
|
-
@imap.append(mailbox, message.rfc822, message.flags, message.internaldate)
|
108
|
-
end
|
109
|
-
|
110
|
-
true
|
111
|
-
end
|
112
|
-
alias << append
|
113
|
-
|
114
82
|
# Connects to the IMAP server and logs in if a connection hasn't already been
|
115
83
|
# established.
|
116
84
|
def connect
|
117
|
-
return if @
|
85
|
+
return if @conn
|
118
86
|
safely {} # connect, but do nothing else
|
119
87
|
end
|
120
88
|
|
89
|
+
# Gets the server's mailbox hierarchy delimiter.
|
90
|
+
def delim
|
91
|
+
@delim ||= safely { @conn.list('', '')[0].delim }
|
92
|
+
end
|
93
|
+
|
121
94
|
# Closes the IMAP connection if one is currently open.
|
122
95
|
def disconnect
|
123
|
-
return unless @
|
96
|
+
return unless @conn
|
124
97
|
|
125
|
-
|
126
|
-
|
98
|
+
begin
|
99
|
+
@conn.disconnect
|
100
|
+
rescue Errno::ENOTCONN => e
|
101
|
+
debug "#{e.class.name}: #{e.message}"
|
102
|
+
end
|
103
|
+
|
104
|
+
reset
|
127
105
|
|
128
106
|
info "disconnected"
|
129
107
|
end
|
130
|
-
synchronized :disconnect
|
131
|
-
|
132
|
-
# Iterates through Larch message ids in this mailbox, yielding each one to the
|
133
|
-
# provided block.
|
134
|
-
def each
|
135
|
-
ids = @mutex.synchronize do
|
136
|
-
unsync_scan_mailbox
|
137
|
-
@ids
|
138
|
-
end
|
139
108
|
|
140
|
-
|
109
|
+
# Iterates through all mailboxes in the account, yielding each one as a
|
110
|
+
# Larch::IMAP::Mailbox instance to the given block.
|
111
|
+
def each_mailbox
|
112
|
+
update_mailboxes
|
113
|
+
@mailboxes.each_value {|mailbox| yield mailbox }
|
141
114
|
end
|
142
115
|
|
143
|
-
# Gets
|
144
|
-
def
|
145
|
-
|
146
|
-
uid = @ids[message_id]
|
147
|
-
|
148
|
-
raise NotFoundError, "message not found: #{message_id}" if uid.nil?
|
149
|
-
|
150
|
-
debug "fetching envelope: #{message_id}"
|
151
|
-
imap_uid_fetch([uid], 'ENVELOPE').first.attr['ENVELOPE']
|
116
|
+
# Gets the IMAP hostname.
|
117
|
+
def host
|
118
|
+
@uri.host
|
152
119
|
end
|
153
120
|
|
154
|
-
#
|
155
|
-
#
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
raise NotFoundError, "message not found: #{message_id}" if uid.nil?
|
121
|
+
# Gets a Larch::IMAP::Mailbox instance representing the specified mailbox. If
|
122
|
+
# the mailbox doesn't exist and the <tt>:create_mailbox</tt> option is
|
123
|
+
# +false+, or if <tt>:create_mailbox</tt> is +true+ and mailbox creation
|
124
|
+
# fails, a Larch::IMAP::MailboxNotFoundError will be raised.
|
125
|
+
def mailbox(name, delim = '/')
|
126
|
+
retries = 0
|
161
127
|
|
162
|
-
|
163
|
-
data = imap_uid_fetch([uid], [(peek ? 'BODY.PEEK[]' : 'BODY[]'), 'FLAGS', 'INTERNALDATE', 'ENVELOPE']).first
|
128
|
+
name = name.gsub(delim, self.delim)
|
164
129
|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
130
|
+
begin
|
131
|
+
@mailboxes.fetch(name) do
|
132
|
+
update_mailboxes
|
133
|
+
return @mailboxes[name] if @mailboxes.has_key?(name)
|
134
|
+
raise MailboxNotFoundError, "mailbox not found: #{name}"
|
135
|
+
end
|
169
136
|
|
170
|
-
|
171
|
-
|
172
|
-
def has_message?(message_id)
|
173
|
-
scan_mailbox
|
174
|
-
@ids.has_key?(message_id)
|
175
|
-
end
|
137
|
+
rescue MailboxNotFoundError => e
|
138
|
+
raise unless @options[:create_mailbox] && retries == 0
|
176
139
|
|
177
|
-
|
178
|
-
|
179
|
-
@uri.host
|
180
|
-
end
|
140
|
+
info "creating mailbox: #{name}"
|
141
|
+
safely { @conn.create(name) } unless @options[:dry_run]
|
181
142
|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
@ids.length
|
143
|
+
retries += 1
|
144
|
+
retry
|
145
|
+
end
|
186
146
|
end
|
187
|
-
alias size length
|
188
147
|
|
189
|
-
#
|
190
|
-
def
|
191
|
-
|
192
|
-
mb.nil? || mb.empty? ? 'INBOX' : CGI.unescape(mb)
|
148
|
+
# Sends an IMAP NOOP command.
|
149
|
+
def noop
|
150
|
+
safely { @conn.noop }
|
193
151
|
end
|
194
152
|
|
195
|
-
#
|
196
|
-
def
|
197
|
-
|
153
|
+
# Gets the IMAP password.
|
154
|
+
def password
|
155
|
+
CGI.unescape(@uri.password)
|
198
156
|
end
|
199
157
|
|
200
158
|
# Gets the IMAP port number.
|
@@ -202,53 +160,53 @@ class IMAP
|
|
202
160
|
@uri.port || (ssl? ? 993 : 143)
|
203
161
|
end
|
204
162
|
|
205
|
-
#
|
206
|
-
|
207
|
-
|
163
|
+
# Connect if necessary, execute the given block, retry if a recoverable error
|
164
|
+
# occurs, die if an unrecoverable error occurs.
|
165
|
+
def safely
|
166
|
+
safe_connect
|
208
167
|
|
209
|
-
|
210
|
-
begin
|
211
|
-
@imap.examine(mailbox)
|
212
|
-
rescue Net::IMAP::NoResponseError => e
|
213
|
-
return if @options[:create_mailbox]
|
214
|
-
raise FatalError, "unable to open mailbox: #{e.message}"
|
215
|
-
end
|
168
|
+
retries = 0
|
216
169
|
|
217
|
-
|
218
|
-
|
170
|
+
begin
|
171
|
+
yield
|
219
172
|
|
220
|
-
|
221
|
-
|
173
|
+
rescue Errno::ECONNRESET,
|
174
|
+
Errno::ENOTCONN,
|
175
|
+
Errno::EPIPE,
|
176
|
+
Errno::ETIMEDOUT,
|
177
|
+
Net::IMAP::ByeResponseError,
|
178
|
+
OpenSSL::SSL::SSLError => e
|
222
179
|
|
223
|
-
|
224
|
-
@last_id = last_id
|
180
|
+
raise unless (retries += 1) <= @options[:max_retries]
|
225
181
|
|
226
|
-
|
227
|
-
(@options[:fast_scan] ? ' (fast scan)' : '')
|
182
|
+
info "#{e.class.name}: #{e.message} (reconnecting)"
|
228
183
|
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
end
|
184
|
+
reset
|
185
|
+
sleep 1 * retries
|
186
|
+
safe_connect
|
187
|
+
retry
|
234
188
|
|
235
|
-
|
236
|
-
|
189
|
+
rescue Net::IMAP::BadResponseError,
|
190
|
+
Net::IMAP::NoResponseError,
|
191
|
+
Net::IMAP::ResponseParseError => e
|
237
192
|
|
238
|
-
unless
|
239
|
-
error "UID not in IMAP response for message: #{id}"
|
240
|
-
next
|
241
|
-
end
|
193
|
+
raise unless (retries += 1) <= @options[:max_retries]
|
242
194
|
|
243
|
-
|
244
|
-
envelope = imap_uid_fetch([uid], 'ENVELOPE').first.attr['ENVELOPE']
|
245
|
-
debug "duplicate message? #{id} (Subject: #{envelope.subject})"
|
246
|
-
end
|
195
|
+
info "#{e.class.name}: #{e.message} (will retry)"
|
247
196
|
|
248
|
-
|
197
|
+
sleep 1 * retries
|
198
|
+
retry
|
249
199
|
end
|
200
|
+
|
201
|
+
rescue Larch::Error => e
|
202
|
+
raise
|
203
|
+
|
204
|
+
rescue Net::IMAP::Error => e
|
205
|
+
raise Error, "#{e.class.name}: #{e.message} (giving up)"
|
206
|
+
|
207
|
+
rescue => e
|
208
|
+
raise FatalError, "#{e.class.name}: #{e.message} (cannot recover)"
|
250
209
|
end
|
251
|
-
synchronized :scan_mailbox
|
252
210
|
|
253
211
|
# Gets the SSL status.
|
254
212
|
def ssl?
|
@@ -260,111 +218,55 @@ class IMAP
|
|
260
218
|
@uri.to_s
|
261
219
|
end
|
262
220
|
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
#
|
268
|
-
# If the given message data includes a valid Message-Id header, then that will
|
269
|
-
# be used to generate an MD5 hash. Otherwise, the hash will be generated based
|
270
|
-
# on the message's RFC822.SIZE and INTERNALDATE.
|
271
|
-
def create_id(data)
|
272
|
-
['RFC822.SIZE', 'INTERNALDATE'].each do |a|
|
273
|
-
raise FatalError, "requested data not in IMAP response: #{a}" unless data.attr[a]
|
274
|
-
end
|
275
|
-
|
276
|
-
if data.attr['BODY[HEADER.FIELDS (MESSAGE-ID)]'] =~ REGEX_MESSAGE_ID
|
277
|
-
Digest::MD5.hexdigest($1)
|
278
|
-
else
|
279
|
-
Digest::MD5.hexdigest(sprintf('%d%d', data.attr['RFC822.SIZE'],
|
280
|
-
Time.parse(data.attr['INTERNALDATE']).to_i))
|
281
|
-
end
|
221
|
+
# Gets the IMAP mailbox specified in the URI, or +nil+ if none.
|
222
|
+
def uri_mailbox
|
223
|
+
mb = @uri.path[1..-1]
|
224
|
+
mb.nil? || mb.empty? ? nil : CGI.unescape(mb)
|
282
225
|
end
|
283
226
|
|
284
|
-
#
|
285
|
-
|
286
|
-
|
287
|
-
data = safely { @imap.fetch(ids, fields) }
|
288
|
-
|
289
|
-
# If fields isn't an array, make it one.
|
290
|
-
fields = REGEX_FIELDS.match(fields).captures unless fields.is_a?(Array)
|
291
|
-
|
292
|
-
# Translate BODY.PEEK to BODY in fields, since that's how it'll come back in
|
293
|
-
# the response.
|
294
|
-
fields.map! {|f| f.sub(/^BODY\.PEEK\[/, 'BODY[') }
|
295
|
-
|
296
|
-
good_results = ids.respond_to?(:member?) ?
|
297
|
-
data.find_all {|i| ids.member?(i.seqno) && fields.all? {|f| i.attr.member?(f) }} :
|
298
|
-
data.find_all {|i| ids == i.seqno && fields.all? {|f| i.attr.member?(f) }}
|
299
|
-
|
300
|
-
if good_results.empty?
|
301
|
-
raise FatalError, "0 out of #{data.length} items in IMAP response for message(s) #{ids} contained all requested fields: #{fields.join(', ')}"
|
302
|
-
elsif good_results.length < data.length
|
303
|
-
error "IMAP server sent #{good_results.length} results in response to a request for #{data.length} messages"
|
304
|
-
end
|
305
|
-
|
306
|
-
good_results
|
227
|
+
# Gets the IMAP username.
|
228
|
+
def username
|
229
|
+
CGI.unescape(@uri.user)
|
307
230
|
end
|
308
231
|
|
309
|
-
|
310
|
-
# server.
|
311
|
-
def imap_uid_fetch(uids, fields)
|
312
|
-
data = safely { @imap.uid_fetch(uids, fields) }
|
313
|
-
|
314
|
-
# If fields isn't an array, make it one.
|
315
|
-
fields = REGEX_FIELDS.match(fields).captures unless fields.is_a?(Array)
|
316
|
-
|
317
|
-
# Translate BODY.PEEK to BODY in fields, since that's how it'll come back in
|
318
|
-
# the response.
|
319
|
-
fields.map! {|f| f.sub(/^BODY\.PEEK\[/, 'BODY[') }
|
320
|
-
|
321
|
-
good_results = data.find_all do |i|
|
322
|
-
i.attr.member?('UID') && uids.member?(i.attr['UID']) &&
|
323
|
-
fields.all? {|f| i.attr.member?(f) }
|
324
|
-
end
|
232
|
+
private
|
325
233
|
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
234
|
+
# Resets the connection and mailbox state.
|
235
|
+
def reset
|
236
|
+
@mutex.synchronize do
|
237
|
+
@conn = nil
|
238
|
+
@mailboxes.each_value {|mb| mb.reset }
|
330
239
|
end
|
331
|
-
|
332
|
-
good_results
|
333
240
|
end
|
334
241
|
|
335
|
-
|
336
|
-
|
337
|
-
|
242
|
+
def safe_connect
|
243
|
+
return if @conn
|
244
|
+
|
338
245
|
retries = 0
|
339
246
|
|
340
247
|
begin
|
341
|
-
unsafe_connect
|
342
|
-
rescue *RECOVERABLE_ERRORS => e
|
343
|
-
info "#{e.class.name}: #{e.message} (will retry)"
|
344
|
-
raise unless (retries += 1) <= @options[:max_retries]
|
248
|
+
unsafe_connect
|
345
249
|
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
250
|
+
rescue Errno::ECONNRESET,
|
251
|
+
Errno::EPIPE,
|
252
|
+
Errno::ETIMEDOUT,
|
253
|
+
OpenSSL::SSL::SSLError => e
|
350
254
|
|
351
|
-
|
255
|
+
raise unless (retries += 1) <= @options[:max_retries]
|
256
|
+
|
257
|
+
# Special check to ensure that we don't retry on OpenSSL certificate
|
258
|
+
# verification errors.
|
259
|
+
raise if e.is_a?(OpenSSL::SSL::SSLError) && e.message =~ /certificate verify failed/
|
352
260
|
|
353
|
-
begin
|
354
|
-
yield
|
355
|
-
rescue *RECOVERABLE_ERRORS => e
|
356
261
|
info "#{e.class.name}: #{e.message} (will retry)"
|
357
|
-
raise unless (retries += 1) <= @options[:max_retries]
|
358
262
|
|
263
|
+
reset
|
359
264
|
sleep 1 * retries
|
360
265
|
retry
|
361
266
|
end
|
362
267
|
|
363
|
-
rescue
|
364
|
-
raise
|
365
|
-
|
366
|
-
rescue IOError, Net::IMAP::Error, OpenSSL::SSL::SSLError, SocketError, SystemCallError => e
|
367
|
-
raise FatalError, "#{e.class.name}: #{e.message} (giving up)"
|
268
|
+
rescue => e
|
269
|
+
raise FatalError, "#{e.class.name}: #{e.message} (cannot recover)"
|
368
270
|
end
|
369
271
|
|
370
272
|
def unsafe_connect
|
@@ -374,15 +276,18 @@ class IMAP
|
|
374
276
|
|
375
277
|
Thread.new do
|
376
278
|
begin
|
377
|
-
@
|
279
|
+
@conn = Net::IMAP.new(host, port, ssl?,
|
280
|
+
ssl? && @options[:ssl_verify] ? @options[:ssl_certs] : nil,
|
281
|
+
@options[:ssl_verify])
|
378
282
|
|
379
283
|
info "connected on port #{port}" << (ssl? ? ' using SSL' : '')
|
380
284
|
|
381
285
|
auth_methods = ['PLAIN']
|
382
286
|
tried = []
|
287
|
+
capability = @conn.capability
|
383
288
|
|
384
289
|
['LOGIN', 'CRAM-MD5'].each do |method|
|
385
|
-
auth_methods << method if
|
290
|
+
auth_methods << method if capability.include?("AUTH=#{method}")
|
386
291
|
end
|
387
292
|
|
388
293
|
begin
|
@@ -391,9 +296,9 @@ class IMAP
|
|
391
296
|
debug "authenticating using #{method}"
|
392
297
|
|
393
298
|
if method == 'PLAIN'
|
394
|
-
@
|
299
|
+
@conn.login(username, password)
|
395
300
|
else
|
396
|
-
@
|
301
|
+
@conn.authenticate(method, username, password)
|
397
302
|
end
|
398
303
|
|
399
304
|
info "authenticated using #{method}"
|
@@ -407,12 +312,28 @@ class IMAP
|
|
407
312
|
|
408
313
|
rescue => e
|
409
314
|
exception = e
|
410
|
-
error e.message
|
411
315
|
end
|
412
316
|
end.join
|
413
317
|
|
414
318
|
raise exception if exception
|
415
319
|
end
|
320
|
+
|
321
|
+
def update_mailboxes
|
322
|
+
all = safely { @conn.list('', '*') } || []
|
323
|
+
subscribed = safely { @conn.lsub('', '*') } || []
|
324
|
+
|
325
|
+
@mutex.synchronize do
|
326
|
+
# Remove cached mailboxes that no longer exist.
|
327
|
+
@mailboxes.delete_if {|k, v| !all.any?{|mb| mb.name == k}}
|
328
|
+
|
329
|
+
# Update cached mailboxes.
|
330
|
+
all.each do |mb|
|
331
|
+
@mailboxes[mb.name] ||= Mailbox.new(self, mb.name, mb.delim,
|
332
|
+
subscribed.any?{|s| s.name == mb.name}, mb.attr)
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
416
337
|
end
|
417
338
|
|
418
339
|
end
|
data/lib/larch/logger.rb
CHANGED
@@ -4,11 +4,12 @@ class Logger
|
|
4
4
|
attr_reader :level, :output
|
5
5
|
|
6
6
|
LEVELS = {
|
7
|
-
:fatal
|
8
|
-
:error
|
9
|
-
:warn
|
10
|
-
:info
|
11
|
-
:debug
|
7
|
+
:fatal => 0,
|
8
|
+
:error => 1,
|
9
|
+
:warn => 2,
|
10
|
+
:info => 3,
|
11
|
+
:debug => 4,
|
12
|
+
:insane => 5
|
12
13
|
}
|
13
14
|
|
14
15
|
def initialize(level = :info, output = $stdout)
|