larch 1.1.0.dev.20091106 → 1.1.0.dev.20091123
Sign up to get free protection for your applications and to get access to all the features.
- data/HISTORY +5 -0
- data/bin/larch +6 -10
- data/lib/larch/config.rb +20 -2
- data/lib/larch/db/account.rb +1 -1
- data/lib/larch/db/mailbox.rb +1 -1
- data/lib/larch/db/message.rb +1 -1
- data/lib/larch/imap/mailbox.rb +61 -16
- data/lib/larch/imap.rb +4 -3
- data/lib/larch/version.rb +1 -1
- data/lib/larch.rb +6 -1
- metadata +3 -3
data/HISTORY
CHANGED
@@ -17,8 +17,13 @@ Version 1.1.0 (git)
|
|
17
17
|
mailboxes.
|
18
18
|
* Added short versions of common command-line options.
|
19
19
|
* The --fast-scan option has been removed.
|
20
|
+
* The --to-folder option can now be used in conjunction with --all or
|
21
|
+
--all-subscribed to copy messages from multiple source folders to a single
|
22
|
+
destination folder.
|
20
23
|
* Fixed encoding issues when creating mailboxes and getting mailbox lists.
|
21
24
|
* Fixed incorrect case-sensitive treatment of the 'INBOX' folder name.
|
25
|
+
* Fixed a bug in which Larch would try to copy flags that weren't supported on
|
26
|
+
the destination server.
|
22
27
|
|
23
28
|
Version 1.0.2 (2009-08-05)
|
24
29
|
* Fixed a bug that caused Larch to try to set the read-only \Recent flag on
|
data/bin/larch
CHANGED
@@ -21,11 +21,11 @@ Usage:
|
|
21
21
|
Server Options:
|
22
22
|
EOS
|
23
23
|
opt :from, "URI of the source IMAP server.", :short => '-f', :type => :string
|
24
|
-
opt :from_folder, "Source folder to copy from", :short => '-F', :default => Config::DEFAULT['from-folder']
|
24
|
+
opt :from_folder, "Source folder to copy from (default: INBOX)", :short => '-F', :default => Config::DEFAULT['from-folder'], :type => :string
|
25
25
|
opt :from_pass, "Source server password (default: prompt)", :short => '-p', :type => :string
|
26
26
|
opt :from_user, "Source server username (default: prompt)", :short => '-u', :type => :string
|
27
27
|
opt :to, "URI of the destination IMAP server.", :short => '-t', :type => :string
|
28
|
-
opt :to_folder, "Destination folder to copy to", :short => '-T', :default => Config::DEFAULT['to-folder']
|
28
|
+
opt :to_folder, "Destination folder to copy to (default: INBOX)", :short => '-T', :default => Config::DEFAULT['to-folder'], :type => :string
|
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
|
|
@@ -46,16 +46,13 @@ EOS
|
|
46
46
|
opt :verbosity, "Output verbosity: debug, info, warn, error, or fatal", :short => '-V', :default => Config::DEFAULT['verbosity']
|
47
47
|
end
|
48
48
|
|
49
|
-
# Load config.
|
50
|
-
config = Config.new(ARGV.shift || 'default', options[:config], options)
|
51
|
-
|
52
49
|
if options[:config_given]
|
53
50
|
Trollop.die :config, ": file not found: #{options[:config]}" unless File.exist?(options[:config])
|
54
51
|
end
|
55
52
|
|
56
|
-
#
|
53
|
+
# Load config.
|
57
54
|
begin
|
58
|
-
config.
|
55
|
+
config = Config.new(ARGV.shift || 'default', options[:config], options)
|
59
56
|
rescue Config::Error => e
|
60
57
|
abort "Config error: #{e}"
|
61
58
|
end
|
@@ -65,13 +62,12 @@ EOS
|
|
65
62
|
uri_to = URI(config.to)
|
66
63
|
|
67
64
|
# Use --from-folder and --to-folder unless folders were specified in the URIs.
|
68
|
-
uri_from.path = uri_from.path.empty? ? '/' + CGI.escape(config.from_folder.gsub(/^\//, '')) : uri_from.path
|
69
|
-
uri_to.path = uri_to.path.empty? ? '/' + CGI.escape(config.to_folder.gsub(/^\//, '')) : uri_to.path
|
65
|
+
uri_from.path = uri_from.path.empty? ? '/' + CGI.escape(config.from_folder.gsub(/^\//, '')) : uri_from.path if config.from_folder
|
66
|
+
uri_to.path = uri_to.path.empty? ? '/' + CGI.escape(config.to_folder.gsub(/^\//, '')) : uri_to.path if config.to_folder
|
70
67
|
|
71
68
|
# --all and --all-subscribed options override folders
|
72
69
|
if config.all || config.all_subscribed
|
73
70
|
uri_from.path = ''
|
74
|
-
uri_to.path = ''
|
75
71
|
end
|
76
72
|
|
77
73
|
# Usernames and passwords specified as arguments override those in the URIs
|
data/lib/larch/config.rb
CHANGED
@@ -12,7 +12,7 @@ class Config
|
|
12
12
|
'exclude' => [],
|
13
13
|
'exclude-file' => nil,
|
14
14
|
'from' => nil,
|
15
|
-
'from-folder' =>
|
15
|
+
'from-folder' => nil, # actually INBOX; see validate()
|
16
16
|
'from-pass' => nil,
|
17
17
|
'from-user' => nil,
|
18
18
|
'max-retries' => 3,
|
@@ -20,7 +20,7 @@ class Config
|
|
20
20
|
'ssl-certs' => nil,
|
21
21
|
'ssl-verify' => false,
|
22
22
|
'to' => nil,
|
23
|
-
'to-folder' =>
|
23
|
+
'to-folder' => nil, # actually INBOX; see validate()
|
24
24
|
'to-pass' => nil,
|
25
25
|
'to-user' => nil,
|
26
26
|
'verbosity' => 'info'
|
@@ -36,6 +36,7 @@ class Config
|
|
36
36
|
end
|
37
37
|
|
38
38
|
load_file(filename)
|
39
|
+
validate
|
39
40
|
end
|
40
41
|
|
41
42
|
def fetch(name)
|
@@ -64,6 +65,7 @@ class Config
|
|
64
65
|
fetch(name)
|
65
66
|
end
|
66
67
|
|
68
|
+
# Validates the config and resolves conflicting settings.
|
67
69
|
def validate
|
68
70
|
['from', 'to'].each do |s|
|
69
71
|
raise Error, "'#{s}' must be a valid IMAP URI (e.g. imap://example.com)" unless fetch(s) =~ IMAP::REGEX_URI
|
@@ -77,6 +79,22 @@ class Config
|
|
77
79
|
raise Error, "exclude file not found: #{exclude_file}" unless File.file?(exclude_file)
|
78
80
|
raise Error, "exclude file cannot be read: #{exclude_file}" unless File.readable?(exclude_file)
|
79
81
|
end
|
82
|
+
|
83
|
+
if @cached['all'] || @cached['all-subscribed']
|
84
|
+
# A specific source folder wins over 'all' and 'all-subscribed'
|
85
|
+
if @cached['from-folder']
|
86
|
+
@cached['all'] = false
|
87
|
+
@cached['all-subscribed'] = false
|
88
|
+
@cached['to-folder'] ||= @cached['from-folder']
|
89
|
+
|
90
|
+
elsif @cached['all'] && @cached['all-subscribed']
|
91
|
+
# 'all' wins over 'all-subscribed'
|
92
|
+
@cached['all-subscribed'] = false
|
93
|
+
end
|
94
|
+
else
|
95
|
+
@cached['from-folder'] ||= 'INBOX'
|
96
|
+
@cached['to-folder'] ||= 'INBOX'
|
97
|
+
end
|
80
98
|
end
|
81
99
|
|
82
100
|
private
|
data/lib/larch/db/account.rb
CHANGED
data/lib/larch/db/mailbox.rb
CHANGED
data/lib/larch/db/message.rb
CHANGED
data/lib/larch/imap/mailbox.rb
CHANGED
@@ -2,7 +2,7 @@ module Larch; class IMAP
|
|
2
2
|
|
3
3
|
# Represents an IMAP mailbox.
|
4
4
|
class Mailbox
|
5
|
-
attr_reader :attr, :db_mailbox, :delim, :imap, :name, :state, :subscribed
|
5
|
+
attr_reader :attr, :db_mailbox, :delim, :flags, :imap, :name, :perm_flags, :state, :subscribed
|
6
6
|
|
7
7
|
# Maximum number of message headers to fetch with a single IMAP command.
|
8
8
|
FETCH_BLOCK_SIZE = 1024
|
@@ -20,6 +20,8 @@ class Mailbox
|
|
20
20
|
@name = name
|
21
21
|
@name_utf7 = Net::IMAP.encode_utf7(@name)
|
22
22
|
@delim = delim
|
23
|
+
@flags = []
|
24
|
+
@perm_flags = []
|
23
25
|
@subscribed = subscribed
|
24
26
|
@attr = attr.flatten
|
25
27
|
|
@@ -72,13 +74,20 @@ class Mailbox
|
|
72
74
|
raise Larch::IMAP::Error, "mailbox cannot contain messages: #{@name}"
|
73
75
|
end
|
74
76
|
|
75
|
-
debug "appending message: #{message.guid}"
|
76
|
-
|
77
|
-
# The \Recent flag is read-only, so we shouldn't try to set it at the
|
78
|
-
# destination.
|
79
77
|
flags = message.flags.dup
|
80
|
-
flags.delete(:Recent)
|
81
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
|
+
debug "flag not supported on destination: #{flag}"
|
86
|
+
true
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
debug "appending message: #{message.guid}"
|
82
91
|
@imap.conn.append(@name_utf7, message.rfc822, flags, message.internaldate) unless @imap.options[:dry_run]
|
83
92
|
end
|
84
93
|
|
@@ -90,7 +99,7 @@ class Mailbox
|
|
90
99
|
# of each to the provided block.
|
91
100
|
def each_guid # :yields: guid
|
92
101
|
scan
|
93
|
-
@db_mailbox.
|
102
|
+
@db_mailbox.messages_dataset.each {|db_message| yield db_message.guid }
|
94
103
|
end
|
95
104
|
|
96
105
|
# Iterates through mailboxes that are first-level children of this mailbox,
|
@@ -152,8 +161,8 @@ class Mailbox
|
|
152
161
|
end
|
153
162
|
|
154
163
|
# Same as fetch, but doesn't mark the message as seen.
|
155
|
-
def peek(
|
156
|
-
fetch(
|
164
|
+
def peek(guid)
|
165
|
+
fetch(guid, true)
|
157
166
|
end
|
158
167
|
|
159
168
|
# Resets the mailbox state.
|
@@ -211,17 +220,31 @@ class Mailbox
|
|
211
220
|
if flag_range && flag_range.last - flag_range.first > 0
|
212
221
|
info "fetching latest message flags..."
|
213
222
|
|
223
|
+
# Load the expected UIDs and their flags into a Hash for quicker lookups.
|
214
224
|
expected_uids = {}
|
215
|
-
@db_mailbox.
|
225
|
+
@db_mailbox.messages_dataset.each do |db_message|
|
226
|
+
expected_uids[db_message.uid] = db_message.flags.split(',').map{|f| f.to_sym }
|
227
|
+
end
|
216
228
|
|
217
229
|
imap_uid_fetch(flag_range, "(UID FLAGS)", 16384) do |fetch_data|
|
230
|
+
# Check the fields in the first response to ensure that everything we
|
231
|
+
# asked for is there.
|
232
|
+
check_response_fields(fetch_data.first, 'UID', 'FLAGS') unless fetch_data.empty?
|
233
|
+
|
218
234
|
Larch.db.transaction do
|
219
235
|
fetch_data.each do |data|
|
220
|
-
|
221
|
-
|
236
|
+
uid = data.attr['UID']
|
237
|
+
flags = data.attr['FLAGS']
|
238
|
+
local_flags = expected_uids[uid]
|
239
|
+
|
240
|
+
# If we haven't seen this message before, or if its flags have
|
241
|
+
# changed, update the database.
|
242
|
+
unless local_flags && local_flags == flags
|
243
|
+
@db_mailbox.messages_dataset.filter(:uid => uid).update(
|
244
|
+
:flags => flags.map{|f| f.to_s }.join(','))
|
245
|
+
end
|
222
246
|
|
223
|
-
|
224
|
-
update(:flags => data.attr['FLAGS'].map{|f| f.to_s }.join(','))
|
247
|
+
expected_uids.delete(uid)
|
225
248
|
end
|
226
249
|
end
|
227
250
|
end
|
@@ -233,7 +256,7 @@ class Mailbox
|
|
233
256
|
debug "removing #{expected_uids.length} deleted messages from the database..."
|
234
257
|
|
235
258
|
Larch.db.transaction do
|
236
|
-
expected_uids.
|
259
|
+
expected_uids.each_key do |uid|
|
237
260
|
@db_mailbox.messages_dataset.filter(:uid => uid).destroy
|
238
261
|
end
|
239
262
|
end
|
@@ -378,6 +401,7 @@ class Mailbox
|
|
378
401
|
|
379
402
|
debug "examining mailbox"
|
380
403
|
@imap.conn.examine(@name_utf7)
|
404
|
+
refresh_flags
|
381
405
|
|
382
406
|
@mutex.synchronize { @state = :examined }
|
383
407
|
|
@@ -405,6 +429,7 @@ class Mailbox
|
|
405
429
|
|
406
430
|
debug "selecting mailbox"
|
407
431
|
@imap.conn.select(@name_utf7)
|
432
|
+
refresh_flags
|
408
433
|
|
409
434
|
@mutex.synchronize { @state = :selected }
|
410
435
|
|
@@ -487,7 +512,18 @@ class Mailbox
|
|
487
512
|
blocks.each do |block|
|
488
513
|
data = @imap.safely do
|
489
514
|
imap_examine
|
490
|
-
|
515
|
+
|
516
|
+
begin
|
517
|
+
data = @imap.conn.uid_fetch(block, fields)
|
518
|
+
|
519
|
+
rescue Net::IMAP::NoResponseError => e
|
520
|
+
raise unless e.message == 'Some messages could not be FETCHed (Failure)'
|
521
|
+
|
522
|
+
# Workaround for stupid Gmail shenanigans.
|
523
|
+
warn "Gmail error: '#{e.message}'; continuing anyway"
|
524
|
+
end
|
525
|
+
|
526
|
+
next data
|
491
527
|
end
|
492
528
|
|
493
529
|
yield data unless data.nil?
|
@@ -500,6 +536,15 @@ class Mailbox
|
|
500
536
|
return str =~ REGEX_MESSAGE_ID ? $1 : nil
|
501
537
|
end
|
502
538
|
|
539
|
+
# Refreshes the list of valid flags for this mailbox.
|
540
|
+
def refresh_flags
|
541
|
+
return unless @imap.conn.responses.has_key?('FLAGS') &&
|
542
|
+
@imap.conn.responses.has_key?('PERMANENTFLAGS')
|
543
|
+
|
544
|
+
@flags = Array(@imap.conn.responses['FLAGS'].first)
|
545
|
+
@perm_flags = Array(@imap.conn.responses['PERMANENTFLAGS'].first)
|
546
|
+
end
|
547
|
+
|
503
548
|
end
|
504
549
|
|
505
550
|
end; end
|
data/lib/larch/imap.rb
CHANGED
@@ -54,9 +54,9 @@ class IMAP
|
|
54
54
|
|
55
55
|
raise ArgumentError, "must provide a username and password" unless @uri.user && @uri.password
|
56
56
|
|
57
|
-
@conn
|
58
|
-
@mailboxes
|
59
|
-
@mutex
|
57
|
+
@conn = nil
|
58
|
+
@mailboxes = {}
|
59
|
+
@mutex = Mutex.new
|
60
60
|
|
61
61
|
@db_account = Database::Account.find_or_create(
|
62
62
|
:hostname => host,
|
@@ -173,6 +173,7 @@ class IMAP
|
|
173
173
|
Errno::ENOTCONN,
|
174
174
|
Errno::EPIPE,
|
175
175
|
Errno::ETIMEDOUT,
|
176
|
+
IOError,
|
176
177
|
Net::IMAP::ByeResponseError,
|
177
178
|
OpenSSL::SSL::SSLError => e
|
178
179
|
|
data/lib/larch/version.rb
CHANGED
data/lib/larch.rb
CHANGED
@@ -69,7 +69,12 @@ module Larch
|
|
69
69
|
next if excluded?(mailbox_from.name)
|
70
70
|
next if subscribed_only && !mailbox_from.subscribed?
|
71
71
|
|
72
|
-
|
72
|
+
if imap_to.uri_mailbox
|
73
|
+
mailbox_to = imap_to.mailbox(imap_to.uri_mailbox)
|
74
|
+
else
|
75
|
+
mailbox_to = imap_to.mailbox(mailbox_from.name, mailbox_from.delim)
|
76
|
+
end
|
77
|
+
|
73
78
|
mailbox_to.subscribe if mailbox_from.subscribed?
|
74
79
|
|
75
80
|
copy_messages(mailbox_from, mailbox_to)
|
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.20091123
|
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: 2009-11-
|
12
|
+
date: 2009-11-23 00:00:00 -08:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -30,7 +30,7 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - ~>
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 3.
|
33
|
+
version: 3.6.0
|
34
34
|
version:
|
35
35
|
- !ruby/object:Gem::Dependency
|
36
36
|
name: sqlite3-ruby
|