larch 1.1.0.dev.20091106 → 1.1.0.dev.20091123
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 +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
|