larch 1.0.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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)
|