rgrove-larch 0.0.1.3 → 0.0.1.4
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/bin/larch +31 -12
- data/lib/larch/imap.rb +8 -3
- data/lib/larch.rb +33 -14
- metadata +1 -1
data/bin/larch
CHANGED
@@ -27,21 +27,25 @@ EOS
|
|
27
27
|
# opt :dry_run, "Don't actually do anything.", :short => '-n'
|
28
28
|
opt :fast_scan, "Use a faster (but less accurate) method to scan mailboxes. This may result in messages being re-copied.", :short => :none
|
29
29
|
opt :from_folder, "Source folder to copy from.", :short => :none, :default => 'INBOX'
|
30
|
-
opt :from_pass, "Source server password (
|
31
|
-
opt :from_user, "Source server username (
|
30
|
+
opt :from_pass, "Source server password (Default: prompt).", :short => :none, :type => :string
|
31
|
+
opt :from_user, "Source server username (Default: prompt).", :short => :none, :type => :string
|
32
32
|
opt :no_create_folder, "Don't create destination folders that don't already exist.", :short => :none
|
33
33
|
opt :to_folder, "Destination folder to copy to.", :short => :none, :default => 'INBOX'
|
34
|
-
opt :to_pass, "Destination server password (
|
35
|
-
opt :to_user, "Destination server username (
|
34
|
+
opt :to_pass, "Destination server password (Default: prompt).", :short => :none, :type => :string
|
35
|
+
opt :to_user, "Destination server username (Default: prompt).", :short => :none, :type => :string
|
36
36
|
opt :verbosity, "Output verbosity: debug, info, warn, error, or fatal.", :short => '-V', :default => 'info'
|
37
37
|
end
|
38
38
|
|
39
39
|
# Validate command-line options.
|
40
40
|
[:from, :to].each do |sym|
|
41
|
-
|
41
|
+
unless options[sym] =~ IMAP::REGEX_URI
|
42
|
+
Trollop.die sym, "must be a valid IMAP URI (e.g. imap://example.com)"
|
43
|
+
end
|
42
44
|
end
|
43
45
|
|
44
|
-
|
46
|
+
unless Logger::LEVELS.has_key?(options[:verbosity].to_sym)
|
47
|
+
Trollop.die :verbosity, "must be one of: #{Logger::LEVELS.keys.join(', ')}"
|
48
|
+
end
|
45
49
|
|
46
50
|
# Create URIs.
|
47
51
|
options[:from] = URI(options[:from])
|
@@ -50,16 +54,31 @@ EOS
|
|
50
54
|
options[:to].path = '/' + CGI.escape(options[:to_folder].gsub(/^\//, ''))
|
51
55
|
|
52
56
|
# Prompt for usernames and passwords if necessary.
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
+
unless options[:from_user]
|
58
|
+
options[:from_user] = ask("Source username (#{options[:from].host}): ")
|
59
|
+
end
|
60
|
+
|
61
|
+
unless options[:from_pass]
|
62
|
+
options[:from_pass] = ask("Source password (#{options[:from].host}): ") {|q| q.echo = false }
|
63
|
+
end
|
64
|
+
|
65
|
+
unless options[:to_user]
|
66
|
+
options[:to_user] = ask("Destination username (#{options[:to].host}): ")
|
67
|
+
end
|
68
|
+
|
69
|
+
unless options[:to_pass]
|
70
|
+
options[:to_pass] = ask("Destination password (#{options[:to].host}): ") {|q| q.echo = false }
|
71
|
+
end
|
57
72
|
|
58
73
|
# Go go go!
|
59
74
|
init(options[:verbosity])
|
60
75
|
|
61
|
-
source = IMAP.new(options[:from], options[:from_user], options[:from_pass],
|
62
|
-
|
76
|
+
source = IMAP.new(options[:from], options[:from_user], options[:from_pass],
|
77
|
+
:fast_scan => options[:fast_scan])
|
78
|
+
|
79
|
+
dest = IMAP.new(options[:to], options[:to_user], options[:to_pass],
|
80
|
+
:create_mailbox => !options[:no_create_folder],
|
81
|
+
:fast_scan => options[:fast_scan])
|
63
82
|
|
64
83
|
for sig in [:SIGINT, :SIGQUIT, :SIGTERM]
|
65
84
|
trap(sig) { @log.fatal "Interrupted (#{sig})"; summary; exit }
|
data/lib/larch/imap.rb
CHANGED
@@ -145,7 +145,7 @@ class IMAP
|
|
145
145
|
raise NotFoundError, "message not found: #{message_id}" if uid.nil?
|
146
146
|
|
147
147
|
debug "fetching envelope: #{message_id}"
|
148
|
-
|
148
|
+
imap_uid_fetch([uid], 'ENVELOPE').first.attr['ENVELOPE']
|
149
149
|
end
|
150
150
|
|
151
151
|
# Fetches a Larch::Message instance representing the message with the
|
@@ -157,7 +157,7 @@ class IMAP
|
|
157
157
|
raise NotFoundError, "message not found: #{message_id}" if uid.nil?
|
158
158
|
|
159
159
|
debug "#{peek ? 'peeking at' : 'fetching'} message: #{message_id}"
|
160
|
-
data =
|
160
|
+
data = imap_uid_fetch([uid], [(peek ? 'BODY.PEEK[]' : 'BODY[]'), 'FLAGS', 'INTERNALDATE', 'ENVELOPE']).first
|
161
161
|
|
162
162
|
Message.new(message_id, data.attr['ENVELOPE'], data.attr['BODY[]'],
|
163
163
|
data.attr['FLAGS'], Time.parse(data.attr['INTERNALDATE']))
|
@@ -332,6 +332,7 @@ class IMAP
|
|
332
332
|
begin
|
333
333
|
unsafe_connect unless @imap
|
334
334
|
rescue *RECOVERABLE_ERRORS => e
|
335
|
+
info "#{e.class.name}: #{e.message} (will retry)"
|
335
336
|
raise unless (retries += 1) <= 3
|
336
337
|
|
337
338
|
@imap = nil
|
@@ -344,14 +345,18 @@ class IMAP
|
|
344
345
|
begin
|
345
346
|
yield
|
346
347
|
rescue *RECOVERABLE_ERRORS => e
|
348
|
+
info "#{e.class.name}: #{e.message} (will retry)"
|
347
349
|
raise unless (retries += 1) <= 3
|
348
350
|
|
349
351
|
sleep 2 * retries
|
350
352
|
retry
|
351
353
|
end
|
352
354
|
|
355
|
+
rescue Net::IMAP::NoResponseError => e
|
356
|
+
raise Error, "#{e.class.name}: #{e.message} (giving up)"
|
357
|
+
|
353
358
|
rescue IOError, Net::IMAP::Error, OpenSSL::SSL::SSLError, SocketError, SystemCallError => e
|
354
|
-
raise FatalError, "
|
359
|
+
raise FatalError, "#{e.class.name}: #{e.message} (giving up)"
|
355
360
|
end
|
356
361
|
|
357
362
|
def unsafe_connect
|
data/lib/larch.rb
CHANGED
@@ -34,7 +34,8 @@ module Larch
|
|
34
34
|
raise ArgumentError, "source must be a Larch::IMAP instance" unless source.is_a?(IMAP)
|
35
35
|
raise ArgumentError, "dest must be a Larch::IMAP instance" unless dest.is_a?(IMAP)
|
36
36
|
|
37
|
-
msgq
|
37
|
+
msgq = SizedQueue.new(32)
|
38
|
+
mutex = Mutex.new
|
38
39
|
|
39
40
|
@copied = 0
|
40
41
|
@failed = 0
|
@@ -42,18 +43,30 @@ module Larch
|
|
42
43
|
|
43
44
|
@log.info "copying messages from #{source.uri} to #{dest.uri}"
|
44
45
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
46
|
+
source_scan = Thread.new do
|
47
|
+
source.scan_mailbox
|
48
|
+
end
|
49
|
+
|
50
|
+
dest_scan = Thread.new do
|
51
|
+
dest.scan_mailbox
|
52
|
+
end
|
49
53
|
|
50
|
-
|
54
|
+
source_scan.join
|
55
|
+
dest_scan.join
|
56
|
+
|
57
|
+
source_copy = Thread.new do
|
51
58
|
begin
|
52
|
-
@total = source.length
|
59
|
+
mutex.synchronize { @total = source.length }
|
53
60
|
|
54
61
|
source.each do |id|
|
55
62
|
next if dest.has_message?(id)
|
56
|
-
|
63
|
+
|
64
|
+
begin
|
65
|
+
msgq << source.peek(id)
|
66
|
+
rescue Larch::IMAP::Error => e
|
67
|
+
mutex.synchronize { @failed += 1 }
|
68
|
+
next
|
69
|
+
end
|
57
70
|
end
|
58
71
|
|
59
72
|
rescue => e
|
@@ -64,29 +77,35 @@ module Larch
|
|
64
77
|
end
|
65
78
|
end
|
66
79
|
|
67
|
-
|
80
|
+
dest_copy = Thread.new do
|
68
81
|
begin
|
69
82
|
while msg = msgq.pop do
|
70
83
|
break if msg == :finished
|
71
84
|
|
72
|
-
|
73
|
-
|
85
|
+
if msg.envelope.from
|
86
|
+
env_from = msg.envelope.from.first
|
87
|
+
from = "#{env_from.mailbox}@#{env_from.host}"
|
88
|
+
else
|
89
|
+
from = '?'
|
90
|
+
end
|
91
|
+
|
92
|
+
@log.info "copying message: #{from} - #{msg.envelope.subject}"
|
74
93
|
dest << msg
|
75
94
|
|
76
|
-
@copied += 1
|
95
|
+
mutex.synchronize { @copied += 1 }
|
77
96
|
end
|
78
97
|
|
79
98
|
rescue IMAP::FatalError => e
|
80
99
|
@log.fatal e.message
|
81
100
|
|
82
101
|
rescue => e
|
83
|
-
@failed += 1
|
102
|
+
mutex.synchronize { @failed += 1 }
|
84
103
|
@log.error e.message
|
85
104
|
retry
|
86
105
|
end
|
87
106
|
end
|
88
107
|
|
89
|
-
|
108
|
+
dest_copy.join
|
90
109
|
|
91
110
|
source.disconnect
|
92
111
|
dest.disconnect
|