larch 1.0.2 → 1.1.0dev20091006

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.
@@ -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/'