rgrove-larch 1.0.0 → 1.0.0.1
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/lib/larch/imap.rb +91 -62
- data/lib/larch.rb +7 -8
- metadata +2 -3
- data/lib/larch/util.rb +0 -17
data/lib/larch/imap.rb
CHANGED
@@ -6,6 +6,10 @@ 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
|
+
include MonitorMixin
|
10
|
+
|
11
|
+
# Maximum number of messages to fetch at once.
|
12
|
+
MAX_FETCH_COUNT = 1024
|
9
13
|
|
10
14
|
# Recoverable connection errors.
|
11
15
|
RECOVERABLE_ERRORS = [
|
@@ -52,6 +56,8 @@ class IMAP
|
|
52
56
|
# times. Default is 3.
|
53
57
|
#
|
54
58
|
def initialize(uri, username, password, options = {})
|
59
|
+
super()
|
60
|
+
|
55
61
|
raise ArgumentError, "not an IMAP URI: #{uri}" unless uri.is_a?(URI) || uri =~ REGEX_URI
|
56
62
|
raise ArgumentError, "must provide a username and password" unless username && password
|
57
63
|
raise ArgumentError, "options must be a Hash" unless options.is_a?(Hash)
|
@@ -66,7 +72,6 @@ class IMAP
|
|
66
72
|
@last_id = 0
|
67
73
|
@last_scan = nil
|
68
74
|
@message_ids = nil
|
69
|
-
@mutex = Mutex.new
|
70
75
|
|
71
76
|
# Create private convenience methods (debug, info, warn, etc.) to make
|
72
77
|
# logging easier.
|
@@ -89,18 +94,16 @@ class IMAP
|
|
89
94
|
return false if has_message?(message)
|
90
95
|
|
91
96
|
safely do
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
retry
|
100
|
-
end
|
101
|
-
|
102
|
-
raise
|
97
|
+
begin
|
98
|
+
@imap.select(mailbox)
|
99
|
+
rescue Net::IMAP::NoResponseError => e
|
100
|
+
if @options[:create_mailbox]
|
101
|
+
info "creating mailbox: #{mailbox}"
|
102
|
+
@imap.create(mailbox)
|
103
|
+
retry
|
103
104
|
end
|
105
|
+
|
106
|
+
raise
|
104
107
|
end
|
105
108
|
|
106
109
|
debug "appending message: #{message.id}"
|
@@ -122,20 +125,19 @@ class IMAP
|
|
122
125
|
def disconnect
|
123
126
|
return unless @imap
|
124
127
|
|
125
|
-
|
126
|
-
|
128
|
+
synchronize do
|
129
|
+
@imap.disconnect
|
130
|
+
@imap = nil
|
131
|
+
end
|
127
132
|
|
128
133
|
info "disconnected"
|
129
134
|
end
|
130
|
-
synchronized :disconnect
|
131
135
|
|
132
136
|
# Iterates through Larch message ids in this mailbox, yielding each one to the
|
133
137
|
# provided block.
|
134
138
|
def each
|
135
|
-
|
136
|
-
|
137
|
-
@ids
|
138
|
-
end
|
139
|
+
scan_mailbox
|
140
|
+
ids = @ids
|
139
141
|
|
140
142
|
ids.each_key {|id| yield id }
|
141
143
|
end
|
@@ -204,51 +206,52 @@ class IMAP
|
|
204
206
|
|
205
207
|
# Fetches message headers from the current mailbox.
|
206
208
|
def scan_mailbox
|
207
|
-
|
209
|
+
synchronize do
|
210
|
+
return if @last_scan && (Time.now - @last_scan) < SCAN_INTERVAL
|
208
211
|
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
212
|
+
last_id = safely do
|
213
|
+
begin
|
214
|
+
@imap.examine(mailbox)
|
215
|
+
rescue Net::IMAP::NoResponseError => e
|
216
|
+
return if @options[:create_mailbox]
|
217
|
+
raise FatalError, "unable to open mailbox: #{e.message}"
|
218
|
+
end
|
219
|
+
|
220
|
+
@imap.responses['EXISTS'].last
|
215
221
|
end
|
216
222
|
|
217
|
-
@
|
218
|
-
|
223
|
+
@last_scan = Time.now
|
224
|
+
return if last_id == @last_id
|
219
225
|
|
220
|
-
@
|
221
|
-
|
226
|
+
range = (@last_id + 1)..last_id
|
227
|
+
@last_id = last_id
|
222
228
|
|
223
|
-
|
224
|
-
|
229
|
+
info "fetching message headers #{range}" <<
|
230
|
+
(@options[:fast_scan] ? ' (fast scan)' : '')
|
225
231
|
|
226
|
-
|
227
|
-
|
232
|
+
fields = if @options[:fast_scan]
|
233
|
+
['UID', 'RFC822.SIZE', 'INTERNALDATE']
|
234
|
+
else
|
235
|
+
"(UID BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)] RFC822.SIZE INTERNALDATE)"
|
236
|
+
end
|
228
237
|
|
229
|
-
|
230
|
-
|
231
|
-
else
|
232
|
-
"(UID BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)] RFC822.SIZE INTERNALDATE)"
|
233
|
-
end
|
238
|
+
imap_fetch(range, fields).each do |data|
|
239
|
+
id = create_id(data)
|
234
240
|
|
235
|
-
|
236
|
-
|
241
|
+
unless uid = data.attr['UID']
|
242
|
+
error "UID not in IMAP response for message: #{id}"
|
243
|
+
next
|
244
|
+
end
|
237
245
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
246
|
+
if @ids.has_key?(id) && Larch.log.level == :debug
|
247
|
+
envelope = imap_uid_fetch([uid], 'ENVELOPE').first.attr['ENVELOPE']
|
248
|
+
debug "duplicate message? #{id} (Subject: #{envelope.subject})"
|
249
|
+
end
|
242
250
|
|
243
|
-
|
244
|
-
envelope = imap_uid_fetch([uid], 'ENVELOPE').first.attr['ENVELOPE']
|
245
|
-
debug "duplicate message? #{id} (Subject: #{envelope.subject})"
|
251
|
+
@ids[id] = uid
|
246
252
|
end
|
247
|
-
|
248
|
-
@ids[id] = uid
|
249
253
|
end
|
250
254
|
end
|
251
|
-
synchronized :scan_mailbox
|
252
255
|
|
253
256
|
# Gets the SSL status.
|
254
257
|
def ssl?
|
@@ -284,7 +287,19 @@ class IMAP
|
|
284
287
|
# Fetches the specified _fields_ for the specified message sequence id(s) from
|
285
288
|
# the IMAP server.
|
286
289
|
def imap_fetch(ids, fields)
|
287
|
-
|
290
|
+
ids = ids.to_a
|
291
|
+
|
292
|
+
data = safely do
|
293
|
+
pos = 0
|
294
|
+
results = []
|
295
|
+
|
296
|
+
while pos < ids.length
|
297
|
+
results += @imap.fetch(ids[pos, MAX_FETCH_COUNT], fields)
|
298
|
+
pos += MAX_FETCH_COUNT
|
299
|
+
end
|
300
|
+
|
301
|
+
results
|
302
|
+
end
|
288
303
|
|
289
304
|
# If fields isn't an array, make it one.
|
290
305
|
fields = REGEX_FIELDS.match(fields).captures unless fields.is_a?(Array)
|
@@ -309,7 +324,19 @@ class IMAP
|
|
309
324
|
# Fetches the specified _fields_ for the specified UID(s) from the IMAP
|
310
325
|
# server.
|
311
326
|
def imap_uid_fetch(uids, fields)
|
312
|
-
|
327
|
+
uids = uids.to_a
|
328
|
+
|
329
|
+
data = safely do
|
330
|
+
pos = 0
|
331
|
+
results = []
|
332
|
+
|
333
|
+
while pos < uids.length
|
334
|
+
results += @imap.uid_fetch(uids[pos, MAX_FETCH_COUNT], fields)
|
335
|
+
pos += MAX_FETCH_COUNT
|
336
|
+
end
|
337
|
+
|
338
|
+
results
|
339
|
+
end
|
313
340
|
|
314
341
|
# If fields isn't an array, make it one.
|
315
342
|
fields = REGEX_FIELDS.match(fields).captures unless fields.is_a?(Array)
|
@@ -337,15 +364,17 @@ class IMAP
|
|
337
364
|
def safely
|
338
365
|
retries = 0
|
339
366
|
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
367
|
+
synchronize do
|
368
|
+
begin
|
369
|
+
unsafe_connect unless @imap
|
370
|
+
rescue *RECOVERABLE_ERRORS => e
|
371
|
+
info "#{e.class.name}: #{e.message} (will retry)"
|
372
|
+
raise unless (retries += 1) <= @options[:max_retries]
|
373
|
+
|
374
|
+
@imap = nil
|
375
|
+
sleep 1 * retries
|
376
|
+
retry
|
377
|
+
end
|
349
378
|
end
|
350
379
|
|
351
380
|
retries = 0
|
data/lib/larch.rb
CHANGED
@@ -5,11 +5,10 @@ $:.uniq!
|
|
5
5
|
require 'cgi'
|
6
6
|
require 'digest/md5'
|
7
7
|
require 'net/imap'
|
8
|
-
require '
|
8
|
+
require 'monitor'
|
9
9
|
require 'time'
|
10
10
|
require 'uri'
|
11
11
|
|
12
|
-
require 'larch/util'
|
13
12
|
require 'larch/errors'
|
14
13
|
require 'larch/imap'
|
15
14
|
require 'larch/logger'
|
@@ -23,9 +22,9 @@ module Larch
|
|
23
22
|
def init(log_level = :info)
|
24
23
|
@log = Logger.new(log_level)
|
25
24
|
|
26
|
-
@copied
|
27
|
-
@failed
|
28
|
-
@total
|
25
|
+
@copied = 0
|
26
|
+
@failed = 0
|
27
|
+
@total = 0
|
29
28
|
end
|
30
29
|
|
31
30
|
# Copies messages from _source_ to _dest_ if they don't already exist in
|
@@ -37,9 +36,9 @@ module Larch
|
|
37
36
|
msgq = SizedQueue.new(8)
|
38
37
|
mutex = Mutex.new
|
39
38
|
|
40
|
-
@copied
|
41
|
-
@failed
|
42
|
-
@total
|
39
|
+
@copied = 0
|
40
|
+
@failed = 0
|
41
|
+
@total = 0
|
43
42
|
|
44
43
|
@log.info "copying messages from #{source.uri} to #{dest.uri}"
|
45
44
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rgrove-larch
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.0
|
4
|
+
version: 1.0.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryan Grove
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-
|
12
|
+
date: 2009-03-21 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -47,7 +47,6 @@ files:
|
|
47
47
|
- lib/larch/errors.rb
|
48
48
|
- lib/larch/imap.rb
|
49
49
|
- lib/larch/logger.rb
|
50
|
-
- lib/larch/util.rb
|
51
50
|
- lib/larch/version.rb
|
52
51
|
has_rdoc: false
|
53
52
|
homepage: http://github.com/rgrove/larch/
|
data/lib/larch/util.rb
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
class Module
|
2
|
-
|
3
|
-
# Java-style whole-method synchronization, shamelessly stolen from Sup:
|
4
|
-
# http://sup.rubyforge.org. Assumes the existence of a <tt>@mutex</tt>
|
5
|
-
# variable.
|
6
|
-
def synchronized(*methods)
|
7
|
-
methods.each do |method|
|
8
|
-
class_eval <<-EOF
|
9
|
-
alias unsync_#{method} #{method}
|
10
|
-
def #{method}(*a, &b)
|
11
|
-
@mutex.synchronize { unsync_#{method}(*a, &b) }
|
12
|
-
end
|
13
|
-
EOF
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
end
|