larch 1.1.0.dev.20100120 → 1.1.0.dev.20100206
Sign up to get free protection for your applications and to get access to all the features.
- data/HISTORY +3 -0
- data/README.rdoc +14 -13
- data/bin/larch +4 -2
- data/lib/larch.rb +44 -19
- data/lib/larch/config.rb +2 -0
- data/lib/larch/imap.rb +3 -3
- data/lib/larch/imap/mailbox.rb +54 -24
- data/lib/larch/logger.rb +7 -6
- data/lib/larch/version.rb +2 -2
- metadata +3 -3
data/HISTORY
CHANGED
@@ -18,6 +18,9 @@ Version 1.1.0 (git)
|
|
18
18
|
* Folders are now copied recursively by default.
|
19
19
|
* Progress information is now displayed regularly while scanning large
|
20
20
|
mailboxes.
|
21
|
+
* Added --delete option to delete messages from the source after copying them
|
22
|
+
to the destination, or if they already exist at the destination.
|
23
|
+
* Added --expunge option to expunge deleted messages from the source.
|
21
24
|
* Added --sync-flags option to synchronize message flags (like Seen, Flagged,
|
22
25
|
etc.) from the source server to the destination server for messages that
|
23
26
|
already exist on the destination.
|
data/README.rdoc
CHANGED
@@ -10,7 +10,7 @@ accounts.
|
|
10
10
|
|
11
11
|
*Author*:: Ryan Grove (mailto:ryan@wonko.com)
|
12
12
|
*Version*:: 1.1.0 (git)
|
13
|
-
*Copyright*:: Copyright (c)
|
13
|
+
*Copyright*:: Copyright (c) 2010 Ryan Grove. All rights reserved.
|
14
14
|
*License*:: GPL 2.0 (http://opensource.org/licenses/gpl-2.0.php)
|
15
15
|
*Website*:: http://github.com/rgrove/larch
|
16
16
|
|
@@ -28,7 +28,7 @@ Latest development version:
|
|
28
28
|
|
29
29
|
larch [config section] [options]
|
30
30
|
larch --from <uri> --to <uri> [options]
|
31
|
-
|
31
|
+
|
32
32
|
Server Options:
|
33
33
|
--from, -f <s>: URI of the source IMAP server
|
34
34
|
--from-folder, -F <s>: Source folder to copy from (default: INBOX)
|
@@ -39,13 +39,16 @@ Latest development version:
|
|
39
39
|
--to-pass, -P <s>: Destination server password (default: prompt)
|
40
40
|
--to-user, -U <s>: Destination server username (default: prompt)
|
41
41
|
|
42
|
-
|
42
|
+
Copy Options:
|
43
43
|
--all, -a: Copy all folders recursively
|
44
44
|
--all-subscribed, -s: Copy all subscribed folders recursively
|
45
|
+
--delete, -d: Delete messages from the source after copying
|
46
|
+
them, or if they already exist at the destination
|
45
47
|
--exclude <s+>: List of mailbox names/patterns that shouldn't be
|
46
48
|
copied
|
47
49
|
--exclude-file <s>: Filename containing mailbox names/patterns that
|
48
50
|
shouldn't be copied
|
51
|
+
--expunge, -x: Expunge deleted messages from the source
|
49
52
|
--sync-flags, -S: Sync message flags from the source to the
|
50
53
|
destination for messages that already exist at the
|
51
54
|
destination
|
@@ -270,18 +273,16 @@ fairly well-known trick. That said, as with anything tricky, there are caveats.
|
|
270
273
|
|
271
274
|
==== No hierarchical folders
|
272
275
|
|
273
|
-
Similar to Gmail, Yahoo!
|
274
|
-
|
275
|
-
|
276
|
-
|
276
|
+
Similar to Gmail, Yahoo! Mail doesn't allow hierarchical (nested) folders. If
|
277
|
+
you try to copy a folder hierarchy to Yahoo!, it will work, but you'll end up
|
278
|
+
with a set of folders named "folder" and "folder.subfolder" rather than seeing
|
279
|
+
"subfolder" as an actual subfolder of "folder".
|
277
280
|
|
278
281
|
==== No custom flags
|
279
282
|
|
280
|
-
Yahoo! Mail IMAP doesn't
|
281
|
-
|
282
|
-
|
283
|
-
Also, Larch's <tt>sync-flags</tt> option will not work correctly if you have
|
284
|
-
messages with custom flags.
|
283
|
+
Yahoo! Mail IMAP doesn't support custom message flags, such as the tags and
|
284
|
+
junk/not junk flags used by Thunderbird. When transferring messages with custom
|
285
|
+
flags to a Yahoo! Mail IMAP account, the custom flags will be lost.
|
285
286
|
|
286
287
|
==== Here there be dragons
|
287
288
|
|
@@ -336,7 +337,7 @@ Gray II).
|
|
336
337
|
|
337
338
|
== License
|
338
339
|
|
339
|
-
Copyright (c)
|
340
|
+
Copyright (c) 2010 Ryan Grove <ryan@wonko.com>
|
340
341
|
|
341
342
|
Licensed under the GNU General Public License version 2.0.
|
342
343
|
|
data/bin/larch
CHANGED
@@ -12,7 +12,7 @@ module Larch
|
|
12
12
|
options = Trollop.options do
|
13
13
|
version "Larch #{APP_VERSION}\n" << APP_COPYRIGHT
|
14
14
|
banner <<-EOS
|
15
|
-
Larch
|
15
|
+
Larch copies messages from one IMAP server to another. Awesomely.
|
16
16
|
|
17
17
|
Usage:
|
18
18
|
larch [config section] [options]
|
@@ -29,11 +29,13 @@ EOS
|
|
29
29
|
opt :to_pass, "Destination server password (default: prompt)", :short => '-P', :type => :string
|
30
30
|
opt :to_user, "Destination server username (default: prompt)", :short => '-U', :type => :string
|
31
31
|
|
32
|
-
text "\
|
32
|
+
text "\nCopy Options:"
|
33
33
|
opt :all, "Copy all folders recursively", :short => '-a'
|
34
34
|
opt :all_subscribed, "Copy all subscribed folders recursively", :short => '-s'
|
35
|
+
opt :delete, "Delete messages from the source after copying them, or if they already exist at the destination", :short => '-d'
|
35
36
|
opt :exclude, "List of mailbox names/patterns that shouldn't be copied", :short => :none, :type => :strings, :multi => true
|
36
37
|
opt :exclude_file, "Filename containing mailbox names/patterns that shouldn't be copied", :short => :none, :type => :string
|
38
|
+
opt :expunge, "Expunge deleted messages from the source", :short => '-x'
|
37
39
|
opt :sync_flags, "Sync message flags from the source to the destination for messages that already exist at the destination", :short => '-S'
|
38
40
|
|
39
41
|
text "\nGeneral Options:"
|
data/lib/larch.rb
CHANGED
@@ -38,9 +38,10 @@ module Larch
|
|
38
38
|
Net::IMAP.debug = true if @log.level == :insane
|
39
39
|
|
40
40
|
# Stats
|
41
|
-
@copied
|
42
|
-
@
|
43
|
-
@
|
41
|
+
@copied = 0
|
42
|
+
@deleted = 0
|
43
|
+
@failed = 0
|
44
|
+
@total = 0
|
44
45
|
end
|
45
46
|
|
46
47
|
# Recursively copies all messages in all folders from the source to the
|
@@ -49,9 +50,10 @@ module Larch
|
|
49
50
|
raise ArgumentError, "imap_from must be a Larch::IMAP instance" unless imap_from.is_a?(IMAP)
|
50
51
|
raise ArgumentError, "imap_to must be a Larch::IMAP instance" unless imap_to.is_a?(IMAP)
|
51
52
|
|
52
|
-
@copied
|
53
|
-
@
|
54
|
-
@
|
53
|
+
@copied = 0
|
54
|
+
@deleted = 0
|
55
|
+
@failed = 0
|
56
|
+
@total = 0
|
55
57
|
|
56
58
|
imap_from.each_mailbox do |mailbox_from|
|
57
59
|
next if excluded?(mailbox_from.name)
|
@@ -82,9 +84,10 @@ module Larch
|
|
82
84
|
raise ArgumentError, "imap_from must be a Larch::IMAP instance" unless imap_from.is_a?(IMAP)
|
83
85
|
raise ArgumentError, "imap_to must be a Larch::IMAP instance" unless imap_to.is_a?(IMAP)
|
84
86
|
|
85
|
-
@copied
|
86
|
-
@
|
87
|
-
@
|
87
|
+
@copied = 0
|
88
|
+
@deleted = 0
|
89
|
+
@failed = 0
|
90
|
+
@total = 0
|
88
91
|
|
89
92
|
mailbox_from = imap_from.mailbox(imap_from.uri_mailbox || 'INBOX')
|
90
93
|
mailbox_to = imap_to.mailbox(imap_to.uri_mailbox || 'INBOX')
|
@@ -144,7 +147,7 @@ module Larch
|
|
144
147
|
end
|
145
148
|
|
146
149
|
def summary
|
147
|
-
@log.info "#{@copied} message(s) copied, #{@failed} failed, #{@
|
150
|
+
@log.info "#{@copied} message(s) copied, #{@failed} failed, #{@deleted} deleted out of #{@total} total"
|
148
151
|
end
|
149
152
|
|
150
153
|
|
@@ -182,19 +185,26 @@ module Larch
|
|
182
185
|
|
183
186
|
mailbox_from.each_db_message do |from_db_message|
|
184
187
|
guid = from_db_message.guid
|
188
|
+
uid = from_db_message.uid
|
185
189
|
|
186
190
|
if mailbox_to.has_guid?(guid)
|
187
|
-
next unless @config['sync_flags']
|
188
|
-
|
189
191
|
begin
|
190
|
-
|
192
|
+
if @config['sync_flags']
|
193
|
+
to_db_message = mailbox_to.fetch_db_message(guid)
|
194
|
+
|
195
|
+
if to_db_message.flags != from_db_message.flags
|
196
|
+
new_flags = from_db_message.flags_str
|
197
|
+
new_flags = '(none)' if new_flags.empty?
|
191
198
|
|
192
|
-
|
193
|
-
|
194
|
-
|
199
|
+
@log.info "[>] syncing flags: uid #{uid}: #{new_flags}"
|
200
|
+
mailbox_to.set_flags(guid, from_db_message.flags)
|
201
|
+
end
|
202
|
+
end
|
195
203
|
|
196
|
-
|
197
|
-
|
204
|
+
if @config['delete'] && !from_db_message.flags.include?(:Deleted)
|
205
|
+
@log.info "[<] deleting uid #{uid} (already exists at destination)"
|
206
|
+
mailbox_from.set_flags(guid, [:Deleted], true)
|
207
|
+
@deleted += 1
|
198
208
|
end
|
199
209
|
rescue Larch::IMAP::Error => e
|
200
210
|
@log.error e.message
|
@@ -213,17 +223,32 @@ module Larch
|
|
213
223
|
from = '?'
|
214
224
|
end
|
215
225
|
|
216
|
-
@log.info "copying: #{from} - #{msg.envelope.subject}"
|
226
|
+
@log.info "[>] copying uid #{uid}: #{from} - #{msg.envelope.subject}"
|
217
227
|
|
218
228
|
mailbox_to << msg
|
219
229
|
@copied += 1
|
220
230
|
|
231
|
+
if @config['delete']
|
232
|
+
@log.info "[<] deleting uid #{uid}"
|
233
|
+
mailbox_from.set_flags(guid, [:Deleted], true)
|
234
|
+
@deleted += 1
|
235
|
+
end
|
236
|
+
|
221
237
|
rescue Larch::IMAP::Error => e
|
222
238
|
@failed += 1
|
223
239
|
@log.error e.message
|
224
240
|
next
|
225
241
|
end
|
226
242
|
end
|
243
|
+
|
244
|
+
if @config['expunge']
|
245
|
+
begin
|
246
|
+
@log.debug "[<] expunging deleted messages"
|
247
|
+
mailbox_from.expunge
|
248
|
+
rescue Larch::IMAP::Error => e
|
249
|
+
@log.error e.message
|
250
|
+
end
|
251
|
+
end
|
227
252
|
end
|
228
253
|
|
229
254
|
def db_maintenance
|
data/lib/larch/config.rb
CHANGED
@@ -8,9 +8,11 @@ class Config
|
|
8
8
|
'all-subscribed' => false,
|
9
9
|
'config' => File.join('~', '.larch', 'config.yaml'),
|
10
10
|
'database' => File.join('~', '.larch', 'larch.db'),
|
11
|
+
'delete' => false,
|
11
12
|
'dry-run' => false,
|
12
13
|
'exclude' => [],
|
13
14
|
'exclude-file' => nil,
|
15
|
+
'expunge' => false,
|
14
16
|
'from' => nil,
|
15
17
|
'from-folder' => nil, # actually INBOX; see validate()
|
16
18
|
'from-pass' => nil,
|
data/lib/larch/imap.rb
CHANGED
@@ -198,7 +198,7 @@ class IMAP
|
|
198
198
|
|
199
199
|
raise unless (retries += 1) <= @options[:max_retries]
|
200
200
|
|
201
|
-
|
201
|
+
warning "#{e.class.name}: #{e.message} (reconnecting)"
|
202
202
|
|
203
203
|
reset
|
204
204
|
sleep 1 * retries
|
@@ -211,7 +211,7 @@ class IMAP
|
|
211
211
|
|
212
212
|
raise unless (retries += 1) <= @options[:max_retries]
|
213
213
|
|
214
|
-
|
214
|
+
warning "#{e.class.name}: #{e.message} (will retry)"
|
215
215
|
|
216
216
|
sleep 1 * retries
|
217
217
|
retry
|
@@ -292,7 +292,7 @@ class IMAP
|
|
292
292
|
# verification errors.
|
293
293
|
raise if e.is_a?(OpenSSL::SSL::SSLError) && e.message =~ /certificate verify failed/
|
294
294
|
|
295
|
-
|
295
|
+
warning "#{e.class.name}: #{e.message} (will retry)"
|
296
296
|
|
297
297
|
reset
|
298
298
|
sleep 1 * retries
|
data/lib/larch/imap/mailbox.rb
CHANGED
@@ -74,21 +74,8 @@ class Mailbox
|
|
74
74
|
raise Larch::IMAP::Error, "mailbox cannot contain messages: #{@name}"
|
75
75
|
end
|
76
76
|
|
77
|
-
flags = message.flags.dup
|
78
|
-
|
79
|
-
# Don't set any flags that aren't supported on the destination mailbox.
|
80
|
-
flags.delete_if do |flag|
|
81
|
-
# The \Recent flag is read-only, so we shouldn't try to set it.
|
82
|
-
next true if flag == :Recent
|
83
|
-
|
84
|
-
unless @flags.include?(flag) || @perm_flags.include?(:*) || @perm_flags.include?(flag)
|
85
|
-
info "flag not supported on destination: #{flag}"
|
86
|
-
true
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
77
|
debug "appending message: #{message.guid}"
|
91
|
-
@imap.conn.append(@name_utf7, message.rfc822, flags, message.internaldate) unless @imap.options[:dry_run]
|
78
|
+
@imap.conn.append(@name_utf7, message.rfc822, get_supported_flags(message.flags), message.internaldate) unless @imap.options[:dry_run]
|
92
79
|
end
|
93
80
|
|
94
81
|
true
|
@@ -114,6 +101,19 @@ class Mailbox
|
|
114
101
|
mailboxes.each {|mb| yield mb }
|
115
102
|
end
|
116
103
|
|
104
|
+
# Expunges this mailbox, permanently removing all messages with the \Deleted
|
105
|
+
# flag.
|
106
|
+
def expunge
|
107
|
+
return false unless imap_select
|
108
|
+
|
109
|
+
@imap.safely do
|
110
|
+
debug "expunging deleted messages"
|
111
|
+
|
112
|
+
@last_scan = nil
|
113
|
+
@imap.conn.expunge unless @imap.options[:dry_run]
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
117
|
# Returns a Larch::IMAP::Message struct representing the message with the
|
118
118
|
# specified Larch _guid_, or +nil+ if the specified guid was not found in this
|
119
119
|
# mailbox.
|
@@ -121,7 +121,7 @@ class Mailbox
|
|
121
121
|
scan
|
122
122
|
|
123
123
|
unless db_message = fetch_db_message(guid)
|
124
|
-
|
124
|
+
warning "message not found in local db: #{guid}"
|
125
125
|
return nil
|
126
126
|
end
|
127
127
|
|
@@ -135,7 +135,7 @@ class Mailbox
|
|
135
135
|
data.attr['FLAGS'], Time.parse(data.attr['INTERNALDATE']))
|
136
136
|
end
|
137
137
|
|
138
|
-
|
138
|
+
warning "message not found on server: #{guid}"
|
139
139
|
return nil
|
140
140
|
end
|
141
141
|
alias [] fetch
|
@@ -242,16 +242,28 @@ class Mailbox
|
|
242
242
|
return
|
243
243
|
end
|
244
244
|
|
245
|
-
# Sets the IMAP flags for the message specified by _guid_
|
246
|
-
#
|
247
|
-
#
|
248
|
-
|
245
|
+
# Sets the IMAP flags for the message specified by _guid_. _flags_ should be
|
246
|
+
# an array of symbols for standard flags, strings for custom flags.
|
247
|
+
#
|
248
|
+
# If _merge_ is +true+, the specified flags will be merged with the message's
|
249
|
+
# existing flags. Otherwise, all existing flags will be cleared and replaced
|
250
|
+
# with the specified flags.
|
251
|
+
#
|
252
|
+
# Note that the :Recent flag cannot be manually set or removed.
|
253
|
+
#
|
254
|
+
# Returns +true+ on success, +false+ on failure.
|
255
|
+
def set_flags(guid, flags, merge = false)
|
249
256
|
raise ArgumentError, "flags must be an Array" unless flags.is_a?(Array)
|
250
257
|
|
251
|
-
db_message = fetch_db_message(guid)
|
252
|
-
|
258
|
+
return false unless db_message = fetch_db_message(guid)
|
259
|
+
|
260
|
+
merged_flags = merge ? (db_message.flags + flags).uniq : flags
|
261
|
+
supported_flags = get_supported_flags(merged_flags)
|
253
262
|
|
254
|
-
|
263
|
+
return true if db_message.flags == supported_flags
|
264
|
+
|
265
|
+
return false if !imap_select
|
266
|
+
@imap.safely { @imap.conn.uid_store(db_message.uid, 'FLAGS.SILENT', supported_flags) } unless @imap.options[:dry_run]
|
255
267
|
|
256
268
|
true
|
257
269
|
end
|
@@ -317,6 +329,24 @@ class Mailbox
|
|
317
329
|
end
|
318
330
|
end
|
319
331
|
|
332
|
+
# Returns only the flags from the specified _flags_ array that can be set in
|
333
|
+
# this mailbox. Emits a warning message for any unsupported flags.
|
334
|
+
def get_supported_flags(flags)
|
335
|
+
supported_flags = flags.dup
|
336
|
+
|
337
|
+
supported_flags.delete_if do |flag|
|
338
|
+
# The \Recent flag is read-only, so we shouldn't try to set it.
|
339
|
+
next true if flag == :Recent
|
340
|
+
|
341
|
+
unless @flags.include?(flag) || @perm_flags.include?(:*) || @perm_flags.include?(flag)
|
342
|
+
warning "flag not supported on destination: #{flag}"
|
343
|
+
true
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
supported_flags
|
348
|
+
end
|
349
|
+
|
320
350
|
# Fetches the latest flags from the server for the specified range of message
|
321
351
|
# UIDs.
|
322
352
|
def fetch_flags(flag_range)
|
@@ -563,7 +593,7 @@ class Mailbox
|
|
563
593
|
raise unless e.message == 'Some messages could not be FETCHed (Failure)'
|
564
594
|
|
565
595
|
# Workaround for stupid Gmail shenanigans.
|
566
|
-
|
596
|
+
warning "Gmail error: '#{e.message}'; continuing anyway"
|
567
597
|
end
|
568
598
|
|
569
599
|
next data
|
data/lib/larch/logger.rb
CHANGED
@@ -4,12 +4,13 @@ class Logger
|
|
4
4
|
attr_reader :level, :output
|
5
5
|
|
6
6
|
LEVELS = {
|
7
|
-
:fatal
|
8
|
-
:error
|
9
|
-
:warn
|
10
|
-
:
|
11
|
-
:
|
12
|
-
:
|
7
|
+
:fatal => 0,
|
8
|
+
:error => 1,
|
9
|
+
:warn => 2,
|
10
|
+
:warning => 2,
|
11
|
+
:info => 3,
|
12
|
+
:debug => 4,
|
13
|
+
:insane => 5
|
13
14
|
}
|
14
15
|
|
15
16
|
def initialize(level = :info, output = $stdout)
|
data/lib/larch/version.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
module Larch
|
2
2
|
APP_NAME = 'Larch'
|
3
|
-
APP_VERSION = '1.1.0.dev.
|
3
|
+
APP_VERSION = '1.1.0.dev.20100206'
|
4
4
|
APP_AUTHOR = 'Ryan Grove'
|
5
5
|
APP_EMAIL = 'ryan@wonko.com'
|
6
6
|
APP_URL = 'http://github.com/rgrove/larch/'
|
7
|
-
APP_COPYRIGHT = 'Copyright (c)
|
7
|
+
APP_COPYRIGHT = 'Copyright (c) 2010 Ryan Grove <ryan@wonko.com>. All ' <<
|
8
8
|
'rights reserved.'
|
9
9
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: larch
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.0.dev.
|
4
|
+
version: 1.1.0.dev.20100206
|
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: 2010-
|
12
|
+
date: 2010-02-06 00:00:00 -08:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -104,6 +104,6 @@ rubyforge_project:
|
|
104
104
|
rubygems_version: 1.3.5
|
105
105
|
signing_key:
|
106
106
|
specification_version: 3
|
107
|
-
summary: Larch
|
107
|
+
summary: Larch copies messages from one IMAP server to another. Awesomely.
|
108
108
|
test_files: []
|
109
109
|
|