larch 1.0.2 → 1.1.0dev20091006

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,7 +2,10 @@ module Larch; class IMAP
2
2
 
3
3
  # Represents an IMAP mailbox.
4
4
  class Mailbox
5
- attr_reader :attr, :delim, :imap, :name, :state
5
+ attr_reader :attr, :db_mailbox, :delim, :imap, :name, :state, :subscribed
6
+
7
+ # Maximum number of message headers to fetch with a single IMAP command.
8
+ FETCH_BLOCK_SIZE = 1024
6
9
 
7
10
  # Regex to capture a Message-Id header.
8
11
  REGEX_MESSAGE_ID = /message-id\s*:\s*(\S+)/i
@@ -15,12 +18,11 @@ class Mailbox
15
18
 
16
19
  @imap = imap
17
20
  @name = name
21
+ @name_utf7 = Net::IMAP.encode_utf7(@name)
18
22
  @delim = delim
19
23
  @subscribed = subscribed
20
24
  @attr = attr.flatten
21
25
 
22
- @ids = {}
23
- @last_id = 0
24
26
  @last_scan = nil
25
27
  @mutex = Mutex.new
26
28
 
@@ -28,6 +30,23 @@ class Mailbox
28
30
  # open and read-only), or :selected (mailbox open and read-write).
29
31
  @state = :closed
30
32
 
33
+ # Create/update this mailbox in the database.
34
+ mb_data = {
35
+ :name => @name,
36
+ :delim => @delim,
37
+ :attr => @attr.map{|a| a.to_s }.join(','),
38
+ :subscribed => @subscribed ? 1 : 0
39
+ }
40
+
41
+ @db_mailbox = imap.db_account.mailboxes_dataset.filter(:name => @name).first
42
+
43
+ if @db_mailbox
44
+ @db_mailbox.update(mb_data)
45
+ else
46
+ @db_mailbox = Database::Mailbox.create(mb_data)
47
+ imap.db_account.add_mailbox(@db_mailbox)
48
+ end
49
+
31
50
  # Create private convenience methods (debug, info, warn, etc.) to make
32
51
  # logging easier.
33
52
  Logger::LEVELS.each_key do |level|
@@ -46,71 +65,92 @@ class Mailbox
46
65
  # +false+ if the message already exists in the mailbox.
47
66
  def append(message)
48
67
  raise ArgumentError, "must provide a Larch::IMAP::Message object" unless message.is_a?(Larch::IMAP::Message)
49
- return false if has_message?(message)
68
+ return false if has_guid?(message.guid)
50
69
 
51
70
  @imap.safely do
52
71
  unless imap_select(!!@imap.options[:create_mailbox])
53
72
  raise Larch::IMAP::Error, "mailbox cannot contain messages: #{@name}"
54
73
  end
55
74
 
56
- debug "appending message: #{message.id}"
75
+ debug "appending message: #{message.guid}"
57
76
 
58
77
  # The \Recent flag is read-only, so we shouldn't try to set it at the
59
78
  # destination.
60
79
  flags = message.flags.dup
61
80
  flags.delete(:Recent)
62
81
 
63
- @imap.conn.append(@name, message.rfc822, flags, message.internaldate) unless @imap.options[:dry_run]
82
+ @imap.conn.append(@name_utf7, message.rfc822, flags, message.internaldate) unless @imap.options[:dry_run]
64
83
  end
65
84
 
66
85
  true
67
86
  end
68
87
  alias << append
69
88
 
70
- # Iterates through Larch message ids in this mailbox, yielding each one to the
71
- # provided block.
72
- def each
89
+ # Iterates through messages in this mailbox, yielding the Larch message guid
90
+ # of each to the provided block.
91
+ def each_guid # :yields: guid
73
92
  scan
74
- @ids.dup.each_key {|id| yield id }
93
+ @db_mailbox.messages.each {|db_message| yield db_message.guid }
75
94
  end
76
95
 
77
- # Gets a Net::IMAP::Envelope for the specified message id.
78
- def envelope(message_id)
79
- scan
80
- raise Larch::IMAP::MessageNotFoundError, "message not found: #{message_id}" unless uid = @ids[message_id]
81
-
82
- debug "fetching envelope: #{message_id}"
83
- imap_uid_fetch([uid], 'ENVELOPE').first.attr['ENVELOPE']
96
+ # Iterates through mailboxes that are first-level children of this mailbox,
97
+ # yielding a Larch::IMAP::Mailbox object for each to the provided block.
98
+ def each_mailbox # :yields: mailbox
99
+ mailboxes.each {|mb| yield mb }
84
100
  end
85
101
 
86
- # Fetches a Larch::IMAP::Message struct representing the message with the
87
- # specified Larch message id.
88
- def fetch(message_id, peek = false)
102
+ # Returns a Larch::IMAP::Message struct representing the message with the
103
+ # specified Larch _guid_, or +nil+ if the specified guid was not found in this
104
+ # mailbox.
105
+ def fetch(guid, peek = false)
89
106
  scan
90
- raise Larch::IMAP::MessageNotFoundError, "message not found: #{message_id}" unless uid = @ids[message_id]
91
107
 
92
- debug "#{peek ? 'peeking at' : 'fetching'} message: #{message_id}"
93
- data = imap_uid_fetch([uid], [(peek ? 'BODY.PEEK[]' : 'BODY[]'), 'FLAGS', 'INTERNALDATE', 'ENVELOPE']).first
108
+ unless db_message = @db_mailbox.messages_dataset.filter(:guid => guid).first
109
+ warn "message not found in local db: #{guid}"
110
+ return nil
111
+ end
112
+
113
+ debug "#{peek ? 'peeking at' : 'fetching'} message: #{guid}"
114
+
115
+ imap_uid_fetch([db_message.uid], [(peek ? 'BODY.PEEK[]' : 'BODY[]'), 'FLAGS', 'INTERNALDATE', 'ENVELOPE']) do |fetch_data|
116
+ data = fetch_data.first
117
+ check_response_fields(data, 'BODY[]', 'FLAGS', 'INTERNALDATE', 'ENVELOPE')
94
118
 
95
- Message.new(message_id, data.attr['ENVELOPE'], data.attr['BODY[]'],
96
- data.attr['FLAGS'], Time.parse(data.attr['INTERNALDATE']))
119
+ return Message.new(guid, data.attr['ENVELOPE'], data.attr['BODY[]'],
120
+ data.attr['FLAGS'], Time.parse(data.attr['INTERNALDATE']))
121
+ end
122
+
123
+ warn "message not found on server: #{guid}"
124
+ return nil
97
125
  end
98
126
  alias [] fetch
99
127
 
100
- # Returns +true+ if a message with the specified Larch <em>message_id</em>
101
- # exists in this mailbox, +false+ otherwise.
102
- def has_message?(message_id)
128
+ # Returns +true+ if a message with the specified Larch guid exists in this
129
+ # mailbox, +false+ otherwise.
130
+ def has_guid?(guid)
103
131
  scan
104
- @ids.has_key?(message_id)
132
+ @db_mailbox.messages_dataset.filter(:guid => guid).count > 0
105
133
  end
106
134
 
107
135
  # Gets the number of messages in this mailbox.
108
136
  def length
109
137
  scan
110
- @ids.length
138
+ @db_mailbox.messages_dataset.count
111
139
  end
112
140
  alias size length
113
141
 
142
+ # Returns an Array of Larch::IMAP::Mailbox objects representing mailboxes that
143
+ # are first-level children of this mailbox.
144
+ def mailboxes
145
+ return [] if @attr.include?(:Noinferiors)
146
+
147
+ all = @imap.safely{ @imap.conn.list('', "#{@name_utf7}#{@delim}%") } || []
148
+ subscribed = @imap.safely{ @imap.conn.lsub('', "#{@name_utf7}#{@delim}%") } || []
149
+
150
+ all.map{|mb| Mailbox.new(@imap, mb.name, mb.delim,
151
+ subscribed.any?{|s| s.name == mb.name}, mb.attr) }
152
+ end
153
+
114
154
  # Same as fetch, but doesn't mark the message as seen.
115
155
  def peek(message_id)
116
156
  fetch(message_id, true)
@@ -124,52 +164,152 @@ class Mailbox
124
164
  # Fetches message headers from this mailbox.
125
165
  def scan
126
166
  return if @last_scan && (Time.now - @last_scan) < SCAN_INTERVAL
167
+ first_scan = @last_scan.nil?
168
+ @mutex.synchronize { @last_scan = Time.now }
127
169
 
170
+ # Compare the mailbox's current status with its last known status.
128
171
  begin
129
- return unless imap_examine
172
+ return unless status = imap_status('MESSAGES', 'UIDNEXT', 'UIDVALIDITY')
130
173
  rescue Error => e
131
174
  return if @imap.options[:create_mailbox]
132
175
  raise
133
176
  end
134
177
 
135
- last_id = @imap.safely { @imap.conn.responses['EXISTS'].last }
136
- @mutex.synchronize { @last_scan = Time.now }
137
- return if last_id == @last_id
178
+ flag_range = nil
179
+ full_range = nil
180
+
181
+ if @db_mailbox.uidvalidity && @db_mailbox.uidnext &&
182
+ status['UIDVALIDITY'] == @db_mailbox.uidvalidity
138
183
 
139
- range = (@last_id + 1)..last_id
140
- @mutex.synchronize { @last_id = last_id }
184
+ # The UIDVALIDITY is the same as what we saw last time we scanned this
185
+ # mailbox, which means that all the existing messages in the database are
186
+ # still valid. We only need to request headers for new messages.
187
+ #
188
+ # If this is the first scan of this mailbox during this Larch session,
189
+ # then we'll also update the flags of all messages in the mailbox.
141
190
 
142
- info "fetching message headers #{range}" <<
143
- (@imap.options[:fast_scan] ? ' (fast scan)' : '')
191
+ flag_range = 1...@db_mailbox.uidnext if first_scan
192
+ full_range = @db_mailbox.uidnext...status['UIDNEXT']
144
193
 
145
- fields = if @imap.options[:fast_scan]
146
- ['UID', 'RFC822.SIZE', 'INTERNALDATE']
147
194
  else
148
- "(UID BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)] RFC822.SIZE INTERNALDATE)"
195
+
196
+ # The UIDVALIDITY has changed or this is the first time we've scanned this
197
+ # mailbox (ever). Either way, all existing messages in the database are no
198
+ # longer valid, so we have to throw them out and re-request everything.
199
+
200
+ @db_mailbox.remove_all_messages
201
+ full_range = 1...status['UIDNEXT']
149
202
  end
150
203
 
151
- imap_fetch(range.begin..-1, fields).each do |data|
152
- id = create_id(data)
204
+ @db_mailbox.update(:uidvalidity => status['UIDVALIDITY'])
153
205
 
154
- unless uid = data.attr['UID']
155
- error "UID not in IMAP response for message: #{id}"
156
- next
206
+ return unless flag_range || full_range.last - full_range.first > 0
207
+
208
+ # Open the mailbox for read-only access.
209
+ return unless imap_examine
210
+
211
+ if flag_range && flag_range.last - flag_range.first > 0
212
+ info "fetching latest message flags..."
213
+
214
+ expected_uids = {}
215
+ @db_mailbox.messages.each {|db_message| expected_uids[db_message.uid] = true }
216
+
217
+ imap_uid_fetch(flag_range, "(UID FLAGS)", 16384) do |fetch_data|
218
+ Larch.db.transaction do
219
+ fetch_data.each do |data|
220
+ check_response_fields(data, 'UID', 'FLAGS')
221
+ expected_uids.delete(data.attr['UID'])
222
+
223
+ @db_mailbox.messages_dataset.filter(:uid => data.attr['UID']).
224
+ update(:flags => data.attr['FLAGS'].map{|f| f.to_s }.join(','))
225
+ end
226
+ end
157
227
  end
158
228
 
159
- if Larch.log.level == :debug && @ids.has_key?(id)
160
- envelope = imap_uid_fetch([uid], 'ENVELOPE').first.attr['ENVELOPE']
161
- debug "duplicate message? #{id} (Subject: #{envelope.subject})"
229
+ # Any UIDs that are in the database but weren't in the response have been
230
+ # deleted from the server, so we need to delete them from the database as
231
+ # well.
232
+ unless expected_uids.empty?
233
+ debug "removing #{expected_uids.length} deleted messages from the database..."
234
+
235
+ Larch.db.transaction do
236
+ expected_uids.each do |uid|
237
+ @db_mailbox.messages_dataset.filter(:uid => uid).destroy
238
+ end
239
+ end
162
240
  end
163
241
 
164
- @mutex.synchronize { @ids[id] = uid }
242
+ expected_uids = nil
243
+ fetch_data = nil
244
+ end
245
+
246
+ if full_range && full_range.last - full_range.first > 0
247
+ start = @db_mailbox.messages_dataset.count + 1
248
+ total = status['MESSAGES']
249
+ fetched = 0
250
+ progress = 0
251
+
252
+ show_progress = total - start > FETCH_BLOCK_SIZE * 4
253
+
254
+ info "fetching message headers #{start} through #{total}..."
255
+
256
+ begin
257
+ last_good_uid = nil
258
+
259
+ imap_uid_fetch(full_range, "(UID BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)] RFC822.SIZE INTERNALDATE FLAGS)") do |fetch_data|
260
+ check_response_fields(fetch_data, 'UID', 'RFC822.SIZE', 'INTERNALDATE', 'FLAGS')
261
+
262
+ Larch.db.transaction do
263
+ fetch_data.each do |data|
264
+ uid = data.attr['UID']
265
+
266
+ Database::Message.create(
267
+ :mailbox_id => @db_mailbox.id,
268
+ :guid => create_guid(data),
269
+ :uid => uid,
270
+ :message_id => parse_message_id(data.attr['BODY[HEADER.FIELDS (MESSAGE-ID)]']),
271
+ :rfc822_size => data.attr['RFC822.SIZE'].to_i,
272
+ :internaldate => Time.parse(data.attr['INTERNALDATE']).to_i,
273
+ :flags => data.attr['FLAGS'].map{|f| f.to_s }.join(',')
274
+ )
275
+
276
+ last_good_uid = uid
277
+ end
278
+
279
+ @db_mailbox.update(:uidnext => last_good_uid + 1)
280
+ end
281
+
282
+ if show_progress
283
+ fetched += fetch_data.length
284
+ last_progress = progress
285
+ progress = ((100 / (total - start).to_f) * fetched).round
286
+
287
+ info "#{progress}% complete" if progress > last_progress
288
+ end
289
+ end
290
+
291
+ rescue => e
292
+ # Set this mailbox's uidnext value to the last known good UID that was
293
+ # stored in the database, plus 1. This will allow Larch to resume where
294
+ # the error occurred on the next attempt rather than having to start over.
295
+ @db_mailbox.update(:uidnext => last_good_uid + 1) if last_good_uid
296
+ raise
297
+ end
165
298
  end
299
+
300
+ @db_mailbox.update(:uidnext => status['UIDNEXT'])
301
+ return
166
302
  end
167
303
 
168
304
  # Subscribes to this mailbox.
169
305
  def subscribe(force = false)
170
- return if subscribed? && !force
171
- @imap.safely { @imap.conn.subscribe(@name) } unless @imap.options[:dry_run]
306
+ return false if subscribed? && !force
307
+
308
+ @imap.safely { @imap.conn.subscribe(@name_utf7) } unless @imap.options[:dry_run]
172
309
  @mutex.synchronize { @subscribed = true }
310
+ @db_mailbox.update(:subscribed => 1)
311
+
312
+ true
173
313
  end
174
314
 
175
315
  # Returns +true+ if this mailbox is subscribed, +false+ otherwise.
@@ -179,35 +319,55 @@ class Mailbox
179
319
 
180
320
  # Unsubscribes from this mailbox.
181
321
  def unsubscribe(force = false)
182
- return unless subscribed? || force
183
- @imap.safely { @imap.conn.unsubscribe(@name) } unless @imap.options[:dry_run]
322
+ return false unless subscribed? || force
323
+
324
+ @imap.safely { @imap.conn.unsubscribe(@name_utf7) } unless @imap.options[:dry_run]
184
325
  @mutex.synchronize { @subscribed = false }
326
+ @db_mailbox.update(:subscribed => 0)
327
+
328
+ true
185
329
  end
186
330
 
187
331
  private
188
332
 
189
- # Creates an id suitable for uniquely identifying a specific message across
190
- # servers (we hope).
333
+ # Checks the specified Net::IMAP::FetchData object and raises a
334
+ # Larch::IMAP::Error unless it contains all the specified _fields_.
335
+ #
336
+ # _data_ can be a single object or an Array of objects; if it's an Array, then
337
+ # only the first object in the Array will be checked.
338
+ def check_response_fields(data, *fields)
339
+ check_data = data.is_a?(Array) ? data.first : data
340
+
341
+ fields.each do |f|
342
+ raise Error, "required data not in IMAP response: #{f}" unless check_data.attr.has_key?(f)
343
+ end
344
+
345
+ true
346
+ end
347
+
348
+ # Creates a globally unique id suitable for identifying a specific message
349
+ # on any mail server (we hope) based on the given IMAP FETCH _data_.
191
350
  #
192
351
  # If the given message data includes a valid Message-Id header, then that will
193
352
  # be used to generate an MD5 hash. Otherwise, the hash will be generated based
194
353
  # on the message's RFC822.SIZE and INTERNALDATE.
195
- def create_id(data)
196
- ['RFC822.SIZE', 'INTERNALDATE'].each do |a|
197
- raise Error, "required data not in IMAP response: #{a}" unless data.attr[a]
198
- end
199
-
200
- if data.attr['BODY[HEADER.FIELDS (MESSAGE-ID)]'] =~ REGEX_MESSAGE_ID
201
- Digest::MD5.hexdigest($1)
354
+ def create_guid(data)
355
+ if message_id = parse_message_id(data.attr['BODY[HEADER.FIELDS (MESSAGE-ID)]'])
356
+ Digest::MD5.hexdigest(message_id)
202
357
  else
358
+ check_response_fields(data, 'RFC822.SIZE', 'INTERNALDATE')
359
+
203
360
  Digest::MD5.hexdigest(sprintf('%d%d', data.attr['RFC822.SIZE'],
204
361
  Time.parse(data.attr['INTERNALDATE']).to_i))
205
362
  end
206
363
  end
207
364
 
208
- # Examines the mailbox. If _force_ is true, the mailbox will be examined even
365
+ # Examines this mailbox. If _force_ is true, the mailbox will be examined even
209
366
  # if it is already selected (which isn't necessary unless you want to ensure
210
367
  # that it's in a read-only state).
368
+ #
369
+ # Returns +false+ if this mailbox cannot be examined, which may be the case if
370
+ # the \Noselect attribute is set.
211
371
  def imap_examine(force = false)
212
372
  return false if @attr.include?(:Noselect)
213
373
  return true if @state == :examined || (!force && @state == :selected)
@@ -217,7 +377,7 @@ class Mailbox
217
377
  @mutex.synchronize { @state = :closed }
218
378
 
219
379
  debug "examining mailbox"
220
- @imap.conn.examine(@name)
380
+ @imap.conn.examine(@name_utf7)
221
381
 
222
382
  @mutex.synchronize { @state = :examined }
223
383
 
@@ -229,18 +389,12 @@ class Mailbox
229
389
  return true
230
390
  end
231
391
 
232
- # Fetches the specified _fields_ for the specified _set_ of message sequence
233
- # ids (either a Range or an Array of ids).
234
- def imap_fetch(set, fields)
235
- @imap.safely do
236
- imap_examine
237
- @imap.conn.fetch(set, fields)
238
- end
239
- end
240
-
241
392
  # Selects the mailbox if it is not already selected. If the mailbox does not
242
393
  # exist and _create_ is +true+, it will be created. Otherwise, a
243
394
  # Larch::IMAP::Error will be raised.
395
+ #
396
+ # Returns +false+ if this mailbox cannot be selected, which may be the case if
397
+ # the \Noselect attribute is set.
244
398
  def imap_select(create = false)
245
399
  return false if @attr.include?(:Noselect)
246
400
  return true if @state == :selected
@@ -250,7 +404,7 @@ class Mailbox
250
404
  @mutex.synchronize { @state = :closed }
251
405
 
252
406
  debug "selecting mailbox"
253
- @imap.conn.select(@name)
407
+ @imap.conn.select(@name_utf7)
254
408
 
255
409
  @mutex.synchronize { @state = :selected }
256
410
 
@@ -260,7 +414,7 @@ class Mailbox
260
414
  info "creating mailbox: #{@name}"
261
415
 
262
416
  begin
263
- @imap.conn.create(@name) unless @imap.options[:dry_run]
417
+ @imap.conn.create(@name_utf7) unless @imap.options[:dry_run]
264
418
  retry
265
419
  rescue => e
266
420
  raise Error, "unable to create mailbox: #{e.message}"
@@ -271,15 +425,81 @@ class Mailbox
271
425
  return true
272
426
  end
273
427
 
274
- # Fetches the specified _fields_ for the specified _set_ of UIDs (either a
275
- # Range or an Array of UIDs).
276
- def imap_uid_fetch(set, fields)
428
+ # Sends an IMAP STATUS command and returns the status of the requested
429
+ # attributes. Supported attributes include:
430
+ #
431
+ # - MESSAGES
432
+ # - RECENT
433
+ # - UIDNEXT
434
+ # - UIDVALIDITY
435
+ # - UNSEEN
436
+ def imap_status(*attr)
277
437
  @imap.safely do
278
- imap_examine
279
- @imap.conn.uid_fetch(set, fields)
438
+ begin
439
+ debug "getting mailbox status"
440
+ @imap.conn.status(@name_utf7, attr)
441
+ rescue Net::IMAP::NoResponseError => e
442
+ raise Error, "unable to get status of mailbox: #{e.message}"
443
+ end
280
444
  end
281
445
  end
282
446
 
447
+ # Fetches the specified _fields_ for the specified _set_ of UIDs, which can be
448
+ # a number, Range, or Array of UIDs.
449
+ #
450
+ # If _set_ is a number, an Array containing a single Net::IMAP::FetchData
451
+ # object will be yielded to the given block.
452
+ #
453
+ # If _set_ is a Range or Array of UIDs, Arrays of up to <i>block_size</i>
454
+ # Net::IMAP::FetchData objects will be yielded until all requested messages
455
+ # have been fetched.
456
+ #
457
+ # However, if _set_ is a Range with an end value of -1, a single Array
458
+ # containing all requested messages will be yielded, since it's impossible to
459
+ # divide an infinite range into finite blocks.
460
+ def imap_uid_fetch(set, fields, block_size = FETCH_BLOCK_SIZE, &block) # :yields: fetch_data
461
+ if set.is_a?(Numeric) || (set.is_a?(Range) && set.last < 0)
462
+ data = @imap.safely do
463
+ imap_examine
464
+ @imap.conn.uid_fetch(set, fields)
465
+ end
466
+
467
+ yield data unless data.nil?
468
+ end
469
+
470
+ blocks = []
471
+ pos = 0
472
+
473
+ if set.is_a?(Array)
474
+ while pos < set.length
475
+ blocks += set[pos, block_size]
476
+ pos += block_size
477
+ end
478
+
479
+ elsif set.is_a?(Range)
480
+ pos = set.first - 1
481
+
482
+ while pos < set.last
483
+ blocks << ((pos + 1)..[set.last, pos += block_size].min)
484
+ end
485
+ end
486
+
487
+ blocks.each do |block|
488
+ data = @imap.safely do
489
+ imap_examine
490
+ @imap.conn.uid_fetch(block, fields)
491
+ end
492
+
493
+ yield data unless data.nil?
494
+ end
495
+ end
496
+
497
+ # Parses a Message-Id header out of _str_ and returns it, or +nil+ if _str_
498
+ # doesn't contain a valid Message-Id header.
499
+ def parse_message_id(str)
500
+ return str =~ REGEX_MESSAGE_ID ? $1 : nil
501
+ end
502
+
283
503
  end
284
504
 
285
505
  end; end
data/lib/larch/imap.rb CHANGED
@@ -6,19 +6,19 @@ 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
- attr_reader :conn, :options
9
+ attr_reader :conn, :db_account, :mailboxes, :options
10
10
 
11
11
  # URI format validation regex.
12
12
  REGEX_URI = URI.regexp(['imap', 'imaps'])
13
13
 
14
14
  # Larch::IMAP::Message represents a transferable IMAP message which can be
15
15
  # passed between Larch::IMAP instances.
16
- Message = Struct.new(:id, :envelope, :rfc822, :flags, :internaldate)
16
+ Message = Struct.new(:guid, :envelope, :rfc822, :flags, :internaldate)
17
17
 
18
18
  # Initializes a new Larch::IMAP instance that will connect to the specified
19
19
  # IMAP URI.
20
20
  #
21
- # In addition to the URI, the following options may also be specified:
21
+ # In addition to the URI, the following options may be specified:
22
22
  #
23
23
  # [:create_mailbox]
24
24
  # If +true+, mailboxes that don't already exist will be created if
@@ -30,14 +30,6 @@ class IMAP
30
30
  # that it's not actually possible to simulate mailbox creation, so
31
31
  # +:dry_run+ mode always behaves as if +:create_mailbox+ is +false+.
32
32
  #
33
- # [:fast_scan]
34
- # If +true+, a faster but less accurate method will be used to scan
35
- # mailboxes. This will speed up the initial mailbox scan, but will also
36
- # reduce the effectiveness of the message unique id generator. This is
37
- # probably acceptable when copying a very large mailbox to an empty mailbox,
38
- # but if the destination already contains messages, using this option is not
39
- # advised.
40
- #
41
33
  # [:max_retries]
42
34
  # After a recoverable error occurs, retry the operation up to this many
43
35
  # times. Default is 3.
@@ -62,9 +54,14 @@ class IMAP
62
54
 
63
55
  raise ArgumentError, "must provide a username and password" unless @uri.user && @uri.password
64
56
 
65
- @conn = nil
66
- @mailboxes = {}
67
- @mutex = Mutex.new
57
+ @conn = nil
58
+ @mailboxes = {}
59
+ @mutex = Mutex.new
60
+
61
+ @db_account = Database::Account.find_or_create(
62
+ :hostname => host,
63
+ :username => username
64
+ )
68
65
 
69
66
  # Create private convenience methods (debug, info, warn, etc.) to make
70
67
  # logging easier.
@@ -138,7 +135,7 @@ class IMAP
138
135
  raise unless @options[:create_mailbox] && retries == 0
139
136
 
140
137
  info "creating mailbox: #{name}"
141
- safely { @conn.create(name) } unless @options[:dry_run]
138
+ safely { @conn.create(Net::IMAP.encode_utf7(name)) } unless @options[:dry_run]
142
139
 
143
140
  retries += 1
144
141
  retry
@@ -170,7 +167,8 @@ class IMAP
170
167
  begin
171
168
  yield
172
169
 
173
- rescue Errno::ECONNRESET,
170
+ rescue Errno::ECONNABORTED,
171
+ Errno::ECONNRESET,
174
172
  Errno::ENOTCONN,
175
173
  Errno::EPIPE,
176
174
  Errno::ETIMEDOUT,
@@ -324,14 +322,21 @@ class IMAP
324
322
 
325
323
  @mutex.synchronize do
326
324
  # Remove cached mailboxes that no longer exist.
327
- @mailboxes.delete_if {|k, v| !all.any?{|mb| mb.name == k}}
325
+ @mailboxes.delete_if {|k, v| !all.any?{|mb| Net::IMAP.decode_utf7(mb.name) == k}}
328
326
 
329
327
  # Update cached mailboxes.
330
328
  all.each do |mb|
331
- @mailboxes[mb.name] ||= Mailbox.new(self, mb.name, mb.delim,
329
+ name = Net::IMAP.decode_utf7(mb.name)
330
+
331
+ @mailboxes[name] ||= Mailbox.new(self, name, mb.delim,
332
332
  subscribed.any?{|s| s.name == mb.name}, mb.attr)
333
333
  end
334
334
  end
335
+
336
+ # Remove mailboxes that no longer exist from the database.
337
+ @db_account.mailboxes.each do |db_mailbox|
338
+ db_mailbox.destroy unless @mailboxes.has_key?(db_mailbox.name)
339
+ end
335
340
  end
336
341
 
337
342
  end
data/lib/larch/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  module Larch
2
2
  APP_NAME = 'Larch'
3
- APP_VERSION = '1.0.2'
3
+ APP_VERSION = '1.1.0dev20091006'
4
4
  APP_AUTHOR = 'Ryan Grove'
5
5
  APP_EMAIL = 'ryan@wonko.com'
6
6
  APP_URL = 'http://github.com/rgrove/larch/'