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.
- 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
|