rgrove-larch 1.0.0.9 → 1.0.0.10
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 +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
|
|