rgrove-larch 1.0.0.9 → 1.0.0.10
Sign up to get free protection for your applications and to get access to all the features.
- data/HISTORY +4 -1
- data/bin/larch +21 -3
- data/lib/larch/imap/mailbox.rb +28 -8
- data/lib/larch/imap.rb +13 -5
- data/lib/larch/version.rb +1 -1
- data/lib/larch.rb +53 -22
- metadata +1 -1
data/HISTORY
CHANGED
@@ -5,7 +5,10 @@ Version 1.0.1 (?)
|
|
5
5
|
* Ruby 1.9.1 support.
|
6
6
|
* Much more robust handling of unexpected server disconnects and dropped
|
7
7
|
connections.
|
8
|
-
*
|
8
|
+
* Add --all option to copy all folders recursively.
|
9
|
+
* Add --dry-run option to simulate changes without actually making them.
|
10
|
+
* Add --ssl-certs option to specify a bundle of trusted SSL certificates.
|
11
|
+
* Add --ssl-verify option to verify server SSL certificates.
|
9
12
|
* Fix excessive post-scan processing times for very large mailboxes.
|
10
13
|
* Fetch message headers in blocks of up to 1024 at a time rather than all at
|
11
14
|
once, to prevent potential problems with certain servers when a mailbox
|
data/bin/larch
CHANGED
@@ -23,6 +23,7 @@ EOS
|
|
23
23
|
opt :to, "URI of the destination IMAP server.", :short => '-t', :type => :string, :required => true
|
24
24
|
|
25
25
|
text "\nCopy Options:"
|
26
|
+
opt :all, "Copy all folders recursively", :short => :none
|
26
27
|
opt :from_folder, "Source folder to copy from", :short => :none, :default => 'INBOX'
|
27
28
|
opt :from_pass, "Source server password (default: prompt)", :short => :none, :type => :string
|
28
29
|
opt :from_user, "Source server username (default: prompt)", :short => :none, :type => :string
|
@@ -31,7 +32,7 @@ EOS
|
|
31
32
|
opt :to_user, "Destination server username (default: prompt)", :short => :none, :type => :string
|
32
33
|
|
33
34
|
text "\nGeneral Options:"
|
34
|
-
|
35
|
+
opt :dry_run, "Don't actually make any changes.", :short => '-n'
|
35
36
|
opt :fast_scan, "Use a faster (but less accurate) method to scan mailboxes. This may result in messages being re-copied.", :short => :none
|
36
37
|
opt :max_retries, "Maximum number of times to retry after a recoverable error", :short => :none, :default => 3
|
37
38
|
opt :no_create_folder, "Don't create destination folders that don't already exist", :short => :none
|
@@ -47,6 +48,10 @@ EOS
|
|
47
48
|
end
|
48
49
|
end
|
49
50
|
|
51
|
+
if options[:all_given] && (options[:from_folder_given] || options[:to_folder_given])
|
52
|
+
Trollop.die :all, "may not be used with --from-folder or --to-folder"
|
53
|
+
end
|
54
|
+
|
50
55
|
unless Logger::LEVELS.has_key?(options[:verbosity].to_sym)
|
51
56
|
Trollop.die :verbosity, "must be one of: #{Logger::LEVELS.keys.join(', ')}"
|
52
57
|
end
|
@@ -55,6 +60,13 @@ EOS
|
|
55
60
|
uri_from = URI(options[:from])
|
56
61
|
uri_to = URI(options[:to])
|
57
62
|
|
63
|
+
# --all option overrides folders specified in URIs
|
64
|
+
if options[:all_given]
|
65
|
+
uri_from.path = ''
|
66
|
+
uri_to.path = ''
|
67
|
+
end
|
68
|
+
|
69
|
+
# --from-folder and --to-folder options override folders specified in URIs
|
58
70
|
if options[:from_folder_given] || options[:to_folder_given]
|
59
71
|
uri_from.path = '/' + CGI.escape(options[:from_folder].gsub(/^\//, ''))
|
60
72
|
uri_to.path = '/' + CGI.escape(options[:to_folder].gsub(/^\//, ''))
|
@@ -78,6 +90,7 @@ EOS
|
|
78
90
|
Net::IMAP.debug = true if @log.level == :insane
|
79
91
|
|
80
92
|
imap_from = Larch::IMAP.new(uri_from,
|
93
|
+
:dry_run => options[:dry_run],
|
81
94
|
:fast_scan => options[:fast_scan],
|
82
95
|
:max_retries => options[:max_retries],
|
83
96
|
:ssl_certs => options[:ssl_certs] || nil,
|
@@ -85,7 +98,8 @@ EOS
|
|
85
98
|
)
|
86
99
|
|
87
100
|
imap_to = Larch::IMAP.new(uri_to,
|
88
|
-
:create_mailbox => !options[:no_create_folder],
|
101
|
+
:create_mailbox => !options[:no_create_folder] && !options[:dry_run],
|
102
|
+
:dry_run => options[:dry_run],
|
89
103
|
:fast_scan => options[:fast_scan],
|
90
104
|
:max_retries => options[:max_retries],
|
91
105
|
:ssl_certs => options[:ssl_certs] || nil,
|
@@ -101,5 +115,9 @@ EOS
|
|
101
115
|
end
|
102
116
|
end
|
103
117
|
|
104
|
-
|
118
|
+
if options[:all_given]
|
119
|
+
copy_all(imap_from, imap_to)
|
120
|
+
else
|
121
|
+
copy_folder(imap_from, imap_to)
|
122
|
+
end
|
105
123
|
end
|
data/lib/larch/imap/mailbox.rb
CHANGED
@@ -13,13 +13,14 @@ class Mailbox
|
|
13
13
|
# Minimum time (in seconds) allowed between mailbox scans.
|
14
14
|
SCAN_INTERVAL = 60
|
15
15
|
|
16
|
-
def initialize(imap, name, delim, *attr)
|
16
|
+
def initialize(imap, name, delim, subscribed, *attr)
|
17
17
|
raise ArgumentError, "must provide a Larch::IMAP instance" unless imap.is_a?(Larch::IMAP)
|
18
18
|
|
19
|
-
@
|
20
|
-
@
|
21
|
-
@
|
22
|
-
@
|
19
|
+
@imap = imap
|
20
|
+
@name = name
|
21
|
+
@delim = delim
|
22
|
+
@subscribed = subscribed
|
23
|
+
@attr = attr
|
23
24
|
|
24
25
|
@ids = {}
|
25
26
|
@last_id = 0
|
@@ -54,7 +55,7 @@ class Mailbox
|
|
54
55
|
imap_select(!!@imap.options[:create_mailbox])
|
55
56
|
|
56
57
|
debug "appending message: #{message.id}"
|
57
|
-
@imap.conn.append(@name, message.rfc822, message.flags, message.internaldate)
|
58
|
+
@imap.conn.append(@name, message.rfc822, message.flags, message.internaldate) unless @imap.options[:dry_run]
|
58
59
|
end
|
59
60
|
|
60
61
|
true
|
@@ -161,6 +162,25 @@ class Mailbox
|
|
161
162
|
end
|
162
163
|
end
|
163
164
|
|
165
|
+
# Subscribes to this mailbox.
|
166
|
+
def subscribe(force = false)
|
167
|
+
return if subscribed? && !force
|
168
|
+
@imap.safely { @imap.conn.subscribe(@name) }
|
169
|
+
@mutex.synchronize { @subscribed = true }
|
170
|
+
end
|
171
|
+
|
172
|
+
# Returns +true+ if this mailbox is subscribed, +false+ otherwise.
|
173
|
+
def subscribed?
|
174
|
+
@subscribed
|
175
|
+
end
|
176
|
+
|
177
|
+
# Unsubscribes to this mailbox.
|
178
|
+
def unsubscribe(force = false)
|
179
|
+
return unless subscribed? || force
|
180
|
+
@imap.safely { @imap.conn.unsubscribe(@name) }
|
181
|
+
@mutex.synchronize { @subscribed = false }
|
182
|
+
end
|
183
|
+
|
164
184
|
private
|
165
185
|
|
166
186
|
# Creates an id suitable for uniquely identifying a specific message across
|
@@ -240,10 +260,10 @@ class Mailbox
|
|
240
260
|
rescue Net::IMAP::NoResponseError => e
|
241
261
|
raise Error, "unable to select mailbox: #{e.message}" unless create
|
242
262
|
|
243
|
-
info "creating mailbox: #{
|
263
|
+
info "creating mailbox: #{@name}"
|
244
264
|
|
245
265
|
begin
|
246
|
-
@imap.conn.create(@name)
|
266
|
+
@imap.conn.create(@name) unless @imap.options[:dry_run]
|
247
267
|
retry
|
248
268
|
rescue => e
|
249
269
|
raise Error, "unable to create mailbox: #{e.message}"
|
data/lib/larch/imap.rb
CHANGED
@@ -24,6 +24,12 @@ class IMAP
|
|
24
24
|
# If +true+, mailboxes that don't already exist will be created if
|
25
25
|
# necessary.
|
26
26
|
#
|
27
|
+
# [:dry_run]
|
28
|
+
# If +true+, read-only operations will be performed as usual and all change
|
29
|
+
# operations will be simulated, but no changes will actually be made. Note
|
30
|
+
# that it's not actually possible to simulation mailbox creation, so
|
31
|
+
# +:dry_run+ mode always behaves as if +:create_mailbox+ is +false+.
|
32
|
+
#
|
27
33
|
# [:fast_scan]
|
28
34
|
# If +true+, a faster but less accurate method will be used to scan
|
29
35
|
# mailboxes. This will speed up the initial mailbox scan, but will also
|
@@ -125,7 +131,7 @@ class IMAP
|
|
125
131
|
raise unless @options[:create_mailbox] && retries == 0
|
126
132
|
|
127
133
|
info "creating mailbox: #{name}"
|
128
|
-
safely { @conn.create(name) }
|
134
|
+
safely { @conn.create(name) } unless @options[:dry_run]
|
129
135
|
|
130
136
|
retries += 1
|
131
137
|
retry
|
@@ -306,15 +312,17 @@ class IMAP
|
|
306
312
|
end
|
307
313
|
|
308
314
|
def update_mailboxes
|
309
|
-
|
315
|
+
all = safely { @conn.list('', '*') }
|
316
|
+
subscribed = safely { @conn.lsub('', '*') }
|
310
317
|
|
311
318
|
@mutex.synchronize do
|
312
319
|
# Remove cached mailboxes that no longer exist.
|
313
|
-
@mailboxes.delete_if {|k, v| !
|
320
|
+
@mailboxes.delete_if {|k, v| !all.any?{|mb| mb.name == k}}
|
314
321
|
|
315
322
|
# Update cached mailboxes.
|
316
|
-
|
317
|
-
@mailboxes[mb.name] ||= Mailbox.new(self, mb.name, mb.delim,
|
323
|
+
all.each do |mb|
|
324
|
+
@mailboxes[mb.name] ||= Mailbox.new(self, mb.name, mb.delim,
|
325
|
+
subscribed.any?{|s| s.name == mb.name}, mb.attr)
|
318
326
|
end
|
319
327
|
end
|
320
328
|
end
|
data/lib/larch/version.rb
CHANGED
data/lib/larch.rb
CHANGED
@@ -28,26 +28,70 @@ module Larch
|
|
28
28
|
@total = 0
|
29
29
|
end
|
30
30
|
|
31
|
+
# Recursively copies all messages in all folders from the source to the
|
32
|
+
# destination.
|
33
|
+
def copy_all(imap_from, imap_to)
|
34
|
+
raise ArgumentError, "imap_from must be a Larch::IMAP instance" unless imap_from.is_a?(IMAP)
|
35
|
+
raise ArgumentError, "imap_to must be a Larch::IMAP instance" unless imap_to.is_a?(IMAP)
|
36
|
+
|
37
|
+
imap_from.each_mailbox do |mailbox_from|
|
38
|
+
@copied = 0
|
39
|
+
@failed = 0
|
40
|
+
@total = 0
|
41
|
+
|
42
|
+
mailbox_to = imap_to.mailbox(mailbox_from.name)
|
43
|
+
copy_messages(imap_from, mailbox_from, imap_to, mailbox_to)
|
44
|
+
mailbox_to.subscribe if mailbox_from.subscribed?
|
45
|
+
|
46
|
+
summary
|
47
|
+
end
|
48
|
+
|
49
|
+
rescue => e
|
50
|
+
@log.fatal e.message
|
51
|
+
summary
|
52
|
+
end
|
53
|
+
|
54
|
+
# Copies the messages in a single IMAP folder (non-recursively) from the
|
55
|
+
# source to the destination.
|
31
56
|
def copy_folder(imap_from, imap_to)
|
32
|
-
raise ArgumentError, "
|
33
|
-
raise ArgumentError, "
|
57
|
+
raise ArgumentError, "imap_from must be a Larch::IMAP instance" unless imap_from.is_a?(IMAP)
|
58
|
+
raise ArgumentError, "imap_to must be a Larch::IMAP instance" unless imap_to.is_a?(IMAP)
|
34
59
|
|
35
60
|
@copied = 0
|
36
61
|
@failed = 0
|
37
62
|
@total = 0
|
38
63
|
|
39
|
-
|
40
|
-
|
64
|
+
copy_messages(imap_from, imap_from.mailbox(imap_from.uri_mailbox || 'INBOX'),
|
65
|
+
imap_to, imap_to.mailbox(imap_to.uri_mailbox || 'INBOX'))
|
41
66
|
|
42
|
-
|
67
|
+
imap_from.disconnect
|
68
|
+
imap_to.disconnect
|
69
|
+
|
70
|
+
rescue => e
|
71
|
+
@log.fatal e.message
|
72
|
+
|
73
|
+
ensure
|
74
|
+
summary
|
75
|
+
end
|
76
|
+
|
77
|
+
def summary
|
78
|
+
@log.info "#{@copied} message(s) copied, #{@failed} failed, #{@total - @copied - @failed} untouched out of #{@total} total"
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def copy_messages(imap_from, mailbox_from, imap_to, mailbox_to)
|
84
|
+
raise ArgumentError, "imap_from must be a Larch::IMAP instance" unless imap_from.is_a?(IMAP)
|
85
|
+
raise ArgumentError, "mailbox_from must be a Larch::IMAP::Mailbox instance" unless mailbox_from.is_a?(IMAP::Mailbox)
|
86
|
+
raise ArgumentError, "imap_to must be a Larch::IMAP instance" unless imap_to.is_a?(IMAP)
|
87
|
+
raise ArgumentError, "mailbox_to must be a Larch::IMAP::Mailbox instance" unless mailbox_to.is_a?(IMAP::Mailbox)
|
88
|
+
|
89
|
+
@log.info "copying messages from #{imap_from.host}/#{mailbox_from.name} to #{imap_to.host}/#{mailbox_to.name}"
|
43
90
|
|
44
91
|
imap_from.connect
|
45
92
|
imap_to.connect
|
46
93
|
|
47
|
-
|
48
|
-
mailbox_to = imap_to.mailbox(mailbox_to_name)
|
49
|
-
|
50
|
-
@total = mailbox_from.length
|
94
|
+
@total += mailbox_from.length
|
51
95
|
|
52
96
|
mailbox_from.each do |id|
|
53
97
|
next if mailbox_to.has_message?(id)
|
@@ -74,19 +118,6 @@ module Larch
|
|
74
118
|
next
|
75
119
|
end
|
76
120
|
end
|
77
|
-
|
78
|
-
imap_from.disconnect
|
79
|
-
imap_to.disconnect
|
80
|
-
|
81
|
-
rescue => e
|
82
|
-
@log.fatal e.message
|
83
|
-
|
84
|
-
ensure
|
85
|
-
summary
|
86
|
-
end
|
87
|
-
|
88
|
-
def summary
|
89
|
-
@log.info "#{@copied} message(s) copied, #{@failed} failed, #{@total - @copied - @failed} untouched out of #{@total} total"
|
90
121
|
end
|
91
122
|
end
|
92
123
|
|