larch 1.1.0.dev.20100120 → 1.1.0.dev.20100206
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 +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
|
|