larch 1.0.2 → 1.1.0dev20091006
Sign up to get free protection for your applications and to get access to all the features.
- data/HISTORY +18 -0
- data/README.rdoc +141 -54
- data/bin/larch +55 -80
- data/lib/larch/config.rb +105 -0
- data/lib/larch/db/account.rb +12 -0
- data/lib/larch/db/mailbox.rb +12 -0
- data/lib/larch/db/message.rb +6 -0
- data/lib/larch/db/migrate/001_create_schema.rb +42 -0
- data/lib/larch/errors.rb +4 -0
- data/lib/larch/imap/mailbox.rb +301 -81
- data/lib/larch/imap.rb +23 -18
- data/lib/larch/version.rb +1 -1
- data/lib/larch.rb +88 -26
- metadata +33 -8
data/lib/larch/imap/mailbox.rb
CHANGED
@@ -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
|
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.
|
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(@
|
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
|
71
|
-
# provided block.
|
72
|
-
def
|
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
|
-
@
|
93
|
+
@db_mailbox.messages.each {|db_message| yield db_message.guid }
|
75
94
|
end
|
76
95
|
|
77
|
-
#
|
78
|
-
|
79
|
-
|
80
|
-
|
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
|
-
#
|
87
|
-
# specified Larch
|
88
|
-
|
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
|
-
|
93
|
-
|
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
|
-
|
96
|
-
|
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
|
101
|
-
#
|
102
|
-
def
|
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
|
-
@
|
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
|
-
@
|
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
|
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
|
-
|
136
|
-
|
137
|
-
|
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
|
-
|
140
|
-
|
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
|
-
|
143
|
-
|
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
|
-
|
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
|
-
|
152
|
-
id = create_id(data)
|
204
|
+
@db_mailbox.update(:uidvalidity => status['UIDVALIDITY'])
|
153
205
|
|
154
|
-
|
155
|
-
|
156
|
-
|
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
|
-
|
160
|
-
|
161
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
190
|
-
#
|
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
|
196
|
-
['
|
197
|
-
|
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
|
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(@
|
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(@
|
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(@
|
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
|
-
#
|
275
|
-
#
|
276
|
-
|
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
|
-
|
279
|
-
|
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(:
|
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
|
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
|
66
|
-
@mailboxes
|
67
|
-
@mutex
|
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::
|
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
|
-
|
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
|