rgrove-larch 1.0.0.12 → 1.0.0.13
Sign up to get free protection for your applications and to get access to all the features.
- data/HISTORY +3 -0
- data/README.rdoc +86 -17
- data/bin/larch +32 -7
- data/lib/larch/errors.rb +0 -1
- data/lib/larch/imap.rb +5 -5
- data/lib/larch/version.rb +1 -1
- data/lib/larch.rb +80 -11
- metadata +1 -1
data/HISTORY
CHANGED
@@ -6,7 +6,10 @@ Version 1.0.1 (?)
|
|
6
6
|
* Much more robust handling of unexpected server disconnects and dropped
|
7
7
|
connections.
|
8
8
|
* Added --all option to copy all folders recursively.
|
9
|
+
* Added --all-subscribed option to copy all subscribed folders recursively.
|
9
10
|
* Added --dry-run option to simulate changes without actually making them.
|
11
|
+
* Added --exclude and --exclude-file options to specify folders that should
|
12
|
+
not be copied.
|
10
13
|
* Added --ssl-certs option to specify a bundle of trusted SSL certificates.
|
11
14
|
* Added --ssl-verify option to verify server SSL certificates.
|
12
15
|
* Added a new "insane" logging level, which will output all IMAP commands and
|
data/README.rdoc
CHANGED
@@ -8,7 +8,7 @@ Larch is particularly well-suited for copying email to, from, or between Gmail
|
|
8
8
|
accounts.
|
9
9
|
|
10
10
|
*Author*:: Ryan Grove (mailto:ryan@wonko.com)
|
11
|
-
*Version*::
|
11
|
+
*Version*:: ? (?)
|
12
12
|
*Copyright*:: Copyright (c) 2009 Ryan Grove. All rights reserved.
|
13
13
|
*License*:: GPL 2.0 (http://opensource.org/licenses/gpl-2.0.php)
|
14
14
|
*Website*:: http://github.com/rgrove/larch
|
@@ -21,6 +21,45 @@ Install Larch via RubyGems:
|
|
21
21
|
|
22
22
|
== Usage
|
23
23
|
|
24
|
+
larch --from <uri> --to <uri> [options]
|
25
|
+
|
26
|
+
Required:
|
27
|
+
--from, -f <s>: URI of the source IMAP server.
|
28
|
+
--to, -t <s>: URI of the destination IMAP server.
|
29
|
+
|
30
|
+
Copy Options:
|
31
|
+
--all: Copy all folders recursively
|
32
|
+
--all-subscribed: Copy all subscribed folders recursively
|
33
|
+
--exclude <s+>: List of mailbox names/patterns that shouldn't be
|
34
|
+
copied
|
35
|
+
--exclude-file <s>: Filename containing mailbox names/patterns that
|
36
|
+
shouldn't be copied
|
37
|
+
--from-folder <s>: Source folder to copy from (default: INBOX)
|
38
|
+
--from-pass <s>: Source server password (default: prompt)
|
39
|
+
--from-user <s>: Source server username (default: prompt)
|
40
|
+
--to-folder <s>: Destination folder to copy to (default: INBOX)
|
41
|
+
--to-pass <s>: Destination server password (default: prompt)
|
42
|
+
--to-user <s>: Destination server username (default: prompt)
|
43
|
+
|
44
|
+
General Options:
|
45
|
+
--dry-run, -n: Don't actually make any changes.
|
46
|
+
--fast-scan: Use a faster (but less accurate) method to scan
|
47
|
+
mailboxes. This may result in messages being
|
48
|
+
re-copied.
|
49
|
+
--max-retries <i>: Maximum number of times to retry after a recoverable
|
50
|
+
error (default: 3)
|
51
|
+
--no-create-folder: Don't create destination folders that don't already
|
52
|
+
exist
|
53
|
+
--ssl-certs <s>: Path to a trusted certificate bundle to use to
|
54
|
+
verify server SSL certificates
|
55
|
+
--ssl-verify: Verify server SSL certificates
|
56
|
+
--verbosity, -V <s>: Output verbosity: debug, info, warn, error, or fatal
|
57
|
+
(default: info)
|
58
|
+
--version, -v: Print version and exit
|
59
|
+
--help, -h: Show this message
|
60
|
+
|
61
|
+
== Examples
|
62
|
+
|
24
63
|
Larch is run from the command line. At a minimum, you must specify a source
|
25
64
|
server and a destination server in the form of IMAP URIs. You may also specify
|
26
65
|
one or more options:
|
@@ -37,22 +76,45 @@ the necessary usernames and passwords, then sync the contents of the source's
|
|
37
76
|
|
38
77
|
larch --from imap://mail.example.com --to imap://imap.gmail.com
|
39
78
|
|
40
|
-
To connect using SSL
|
79
|
+
To <b>connect using SSL</b>, specify a URI beginning with <tt>imaps://</tt>:
|
41
80
|
|
42
81
|
larch --from imaps://mail.example.com --to imaps://imap.gmail.com
|
43
82
|
|
44
|
-
If you'd like to sync a folder other than +INBOX+, specify the
|
45
|
-
destination folders using <tt>--from-folder</tt> and
|
83
|
+
If you'd like to <b>sync a specific folder</b> other than +INBOX+, specify the
|
84
|
+
source and destination folders using <tt>--from-folder</tt> and
|
85
|
+
<tt>--to-folder</tt>:
|
46
86
|
|
47
87
|
larch --from imaps://mail.example.com --to imaps://imap.gmail.com \
|
48
88
|
--from-folder "Sent Mail" --to-folder "Sent Mail"
|
49
89
|
|
50
|
-
|
51
|
-
|
90
|
+
To <b>sync all folders</b>, use the <tt>--all</tt> option (or
|
91
|
+
<tt>--all-subscribed</tt> if you only want to <b>sync subscribed folders</b>):
|
92
|
+
|
93
|
+
larch --from imaps://mail.example.com --to imaps://imap.gmail.com --all
|
94
|
+
|
95
|
+
By default Larch will create folders on the destination server if they don't
|
96
|
+
already exist. To prevent this, add the <tt>--no-create-folder</tt> option:
|
97
|
+
|
98
|
+
larch --from imaps://mail.example.com --to imaps://imap.gmail.com --all \
|
99
|
+
--no-create-folder
|
100
|
+
|
101
|
+
You can <b>prevent Larch from syncing one or more folders</b> by using the
|
102
|
+
<tt>--exclude</tt> option:
|
103
|
+
|
104
|
+
larch --from imaps://mail.example.com --to imaps://imap.gmail.com --all \
|
105
|
+
--exclude Spam Trash Drafts "[Gmail]/*"
|
106
|
+
|
107
|
+
If your exclusion list is long or complex, create a text file with one exclusion
|
108
|
+
pattern per line and tell Larch to load it with the <tt>--exclude-file</tt>
|
52
109
|
option:
|
53
110
|
|
54
|
-
larch --from imaps://mail.example.com --to imaps://imap.gmail.com \
|
55
|
-
--
|
111
|
+
larch --from imaps://mail.example.com --to imaps://imap.gmail.com --all \
|
112
|
+
--exclude-file exclude.txt
|
113
|
+
|
114
|
+
The wildcard characters <tt>*</tt> and <tt>?</tt> are supported in exclusion
|
115
|
+
lists. You can also use a regular expression by enclosing a pattern in
|
116
|
+
forward slashes, so the previous example could be achieved with the
|
117
|
+
pattern <tt>/(Spam|Trash|Drafts|\[Gmail\]\/.*)/</tt>
|
56
118
|
|
57
119
|
== Server Compatibility
|
58
120
|
|
@@ -69,15 +131,22 @@ servers:
|
|
69
131
|
|
70
132
|
== Known Issues
|
71
133
|
|
72
|
-
Larch uses Ruby's Net::IMAP standard library for all IMAP operations. While
|
73
|
-
Net::IMAP is generally a very solid library, it contains a bug that can
|
74
|
-
deadlock to occur if a connection drops unexpectedly (either due to
|
75
|
-
issues or because the server closed the connection without warning)
|
76
|
-
server has already begun sending a response and Net::IMAP is
|
77
|
-
more data.
|
78
|
-
|
79
|
-
If this happens, Net::IMAP will continue waiting forever without passing
|
80
|
-
back to Larch, and you will need to manually kill and restart Larch.
|
134
|
+
* Larch uses Ruby's Net::IMAP standard library for all IMAP operations. While
|
135
|
+
Net::IMAP is generally a very solid library, it contains a bug that can
|
136
|
+
cause a deadlock to occur if a connection drops unexpectedly (either due to
|
137
|
+
network issues or because the server closed the connection without warning)
|
138
|
+
when the server has already begun sending a response and Net::IMAP is
|
139
|
+
waiting to receive more data.
|
140
|
+
|
141
|
+
If this happens, Net::IMAP will continue waiting forever without passing
|
142
|
+
control back to Larch, and you will need to manually kill and restart Larch.
|
143
|
+
|
144
|
+
* The Ruby package on Debian, Ubuntu, and some other Debian-based Linux
|
145
|
+
distributions doesn't include the OpenSSL standard library. If you see an
|
146
|
+
error like <tt>uninitialized constant Larch::IMAP::OpenSSL (NameError)</tt>
|
147
|
+
when running Larch, you may need to install the <tt>libopenssl-ruby</tt>
|
148
|
+
package. Please feel free to complain to the maintainer of your distribution's
|
149
|
+
Ruby packages.
|
81
150
|
|
82
151
|
== Support
|
83
152
|
|
data/bin/larch
CHANGED
@@ -24,6 +24,9 @@ EOS
|
|
24
24
|
|
25
25
|
text "\nCopy Options:"
|
26
26
|
opt :all, "Copy all folders recursively", :short => :none
|
27
|
+
opt :all_subscribed, "Copy all subscribed folders recursively", :short => :none
|
28
|
+
opt :exclude, "List of mailbox names/patterns that shouldn't be copied", :short => :none, :type => :strings, :multi => true
|
29
|
+
opt :exclude_file, "Filename containing mailbox names/patterns that shouldn't be copied", :short => :none, :type => :string
|
27
30
|
opt :from_folder, "Source folder to copy from", :short => :none, :default => 'INBOX'
|
28
31
|
opt :from_pass, "Source server password (default: prompt)", :short => :none, :type => :string
|
29
32
|
opt :from_user, "Source server username (default: prompt)", :short => :none, :type => :string
|
@@ -48,20 +51,36 @@ EOS
|
|
48
51
|
end
|
49
52
|
end
|
50
53
|
|
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
|
-
|
55
54
|
unless Logger::LEVELS.has_key?(options[:verbosity].to_sym)
|
56
55
|
Trollop.die :verbosity, "must be one of: #{Logger::LEVELS.keys.join(', ')}"
|
57
56
|
end
|
58
57
|
|
58
|
+
if options[:exclude_file_given]
|
59
|
+
filename = options[:exclude_file]
|
60
|
+
|
61
|
+
Trollop.die :exclude_file, ": file not found: #{filename}" unless File.file?(filename)
|
62
|
+
Trollop.die :exclude_file, ": file cannot be read: #{filename}" unless File.readable?(filename)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Prevent conflicting options from being used.
|
66
|
+
if options[:all_given]
|
67
|
+
[:all_subscribed, :from_folder, :to_folder].each do |o|
|
68
|
+
Trollop.die :all, "may not be used with --#{o.to_s.gsub('_', '-')}" if options["#{o}_given".to_sym]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
if options[:all_subscribed_given]
|
73
|
+
[:all, :from_folder, :to_folder].each do |o|
|
74
|
+
Trollop.die :all_subscribed, "may not be used with --#{o.to_s.gsub('_', '-')}" if options["#{o}_given".to_sym]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
59
78
|
# Create URIs.
|
60
79
|
uri_from = URI(options[:from])
|
61
80
|
uri_to = URI(options[:to])
|
62
81
|
|
63
|
-
# --all
|
64
|
-
if options[:all_given]
|
82
|
+
# --all and --all-subscribed options override folders specified in URIs
|
83
|
+
if options[:all_given] || options[:all_subscribed_given]
|
65
84
|
uri_from.path = ''
|
66
85
|
uri_to.path = ''
|
67
86
|
end
|
@@ -85,7 +104,11 @@ EOS
|
|
85
104
|
uri_to.password ||= CGI.escape(ask("Destination password (#{uri_to.host}): ") {|q| q.echo = false })
|
86
105
|
|
87
106
|
# Go go go!
|
88
|
-
init(
|
107
|
+
init(
|
108
|
+
options[:verbosity],
|
109
|
+
options[:exclude] ? options[:exclude].flatten : [],
|
110
|
+
options[:exclude_file]
|
111
|
+
)
|
89
112
|
|
90
113
|
Net::IMAP.debug = true if @log.level == :insane
|
91
114
|
|
@@ -117,6 +140,8 @@ EOS
|
|
117
140
|
|
118
141
|
if options[:all_given]
|
119
142
|
copy_all(imap_from, imap_to)
|
143
|
+
elsif options[:all_subscribed_given]
|
144
|
+
copy_all(imap_from, imap_to, true)
|
120
145
|
else
|
121
146
|
copy_folder(imap_from, imap_to)
|
122
147
|
end
|
data/lib/larch/errors.rb
CHANGED
data/lib/larch/imap.rb
CHANGED
@@ -119,9 +119,9 @@ class IMAP
|
|
119
119
|
end
|
120
120
|
|
121
121
|
# Gets a Larch::IMAP::Mailbox instance representing the specified mailbox. If
|
122
|
-
# the mailbox doesn't exist and the
|
123
|
-
#
|
124
|
-
# Larch::IMAP::MailboxNotFoundError will be raised.
|
122
|
+
# the mailbox doesn't exist and the <tt>:create_mailbox</tt> option is
|
123
|
+
# +false+, or if <tt>:create_mailbox</tt> is +true+ and mailbox creation
|
124
|
+
# fails, a Larch::IMAP::MailboxNotFoundError will be raised.
|
125
125
|
def mailbox(name, delim = '/')
|
126
126
|
retries = 0
|
127
127
|
|
@@ -160,8 +160,8 @@ class IMAP
|
|
160
160
|
@uri.port || (ssl? ? 993 : 143)
|
161
161
|
end
|
162
162
|
|
163
|
-
# Connect if necessary, execute the given block, retry
|
164
|
-
#
|
163
|
+
# Connect if necessary, execute the given block, retry if a recoverable error
|
164
|
+
# occurs, die if an unrecoverable error occurs.
|
165
165
|
def safely
|
166
166
|
safe_connect
|
167
167
|
|
data/lib/larch/version.rb
CHANGED
data/lib/larch.rb
CHANGED
@@ -17,11 +17,26 @@ require 'larch/version'
|
|
17
17
|
module Larch
|
18
18
|
|
19
19
|
class << self
|
20
|
-
attr_reader :log
|
20
|
+
attr_reader :log, :exclude
|
21
21
|
|
22
|
-
|
22
|
+
EXCLUDE_COMMENT = /#.*$/
|
23
|
+
EXCLUDE_REGEX = /^\s*\/(.*)\/\s*/
|
24
|
+
GLOB_PATTERNS = {'*' => '.*', '?' => '.'}
|
25
|
+
|
26
|
+
def init(log_level = :info, exclude = [], exclude_file = nil)
|
23
27
|
@log = Logger.new(log_level)
|
24
28
|
|
29
|
+
@exclude = exclude.map do |e|
|
30
|
+
if e =~ EXCLUDE_REGEX
|
31
|
+
Regexp.new($1, Regexp::IGNORECASE)
|
32
|
+
else
|
33
|
+
glob_to_regex(e.strip)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
load_exclude_file(exclude_file) if exclude_file
|
38
|
+
|
39
|
+
# Stats
|
25
40
|
@copied = 0
|
26
41
|
@failed = 0
|
27
42
|
@total = 0
|
@@ -29,24 +44,28 @@ module Larch
|
|
29
44
|
|
30
45
|
# Recursively copies all messages in all folders from the source to the
|
31
46
|
# destination.
|
32
|
-
def copy_all(imap_from, imap_to)
|
47
|
+
def copy_all(imap_from, imap_to, subscribed_only = false)
|
33
48
|
raise ArgumentError, "imap_from must be a Larch::IMAP instance" unless imap_from.is_a?(IMAP)
|
34
49
|
raise ArgumentError, "imap_to must be a Larch::IMAP instance" unless imap_to.is_a?(IMAP)
|
35
50
|
|
51
|
+
@copied = 0
|
52
|
+
@failed = 0
|
53
|
+
@total = 0
|
54
|
+
|
36
55
|
imap_from.each_mailbox do |mailbox_from|
|
37
|
-
|
38
|
-
|
39
|
-
@total = 0
|
56
|
+
next if excluded?(mailbox_from.name)
|
57
|
+
next if subscribed_only && !mailbox_from.subscribed?
|
40
58
|
|
41
59
|
mailbox_to = imap_to.mailbox(mailbox_from.name, mailbox_from.delim)
|
42
|
-
copy_messages(imap_from, mailbox_from, imap_to, mailbox_to)
|
43
60
|
mailbox_to.subscribe if mailbox_from.subscribed?
|
44
61
|
|
45
|
-
|
62
|
+
copy_messages(imap_from, mailbox_from, imap_to, mailbox_to)
|
46
63
|
end
|
47
64
|
|
48
65
|
rescue => e
|
49
66
|
@log.fatal e.message
|
67
|
+
|
68
|
+
ensure
|
50
69
|
summary
|
51
70
|
end
|
52
71
|
|
@@ -60,8 +79,13 @@ module Larch
|
|
60
79
|
@failed = 0
|
61
80
|
@total = 0
|
62
81
|
|
63
|
-
|
64
|
-
|
82
|
+
from_name = imap_from.uri_mailbox || 'INBOX'
|
83
|
+
to_name = imap_to.uri_mailbox || 'INBOX'
|
84
|
+
|
85
|
+
return if excluded?(from_name) || excluded?(to_name)
|
86
|
+
|
87
|
+
copy_messages(imap_from, imap_from.mailbox(from_name), imap_to,
|
88
|
+
imap_to.mailbox(to_name))
|
65
89
|
|
66
90
|
imap_from.disconnect
|
67
91
|
imap_to.disconnect
|
@@ -85,6 +109,8 @@ module Larch
|
|
85
109
|
raise ArgumentError, "imap_to must be a Larch::IMAP instance" unless imap_to.is_a?(IMAP)
|
86
110
|
raise ArgumentError, "mailbox_to must be a Larch::IMAP::Mailbox instance" unless mailbox_to.is_a?(IMAP::Mailbox)
|
87
111
|
|
112
|
+
return if excluded?(mailbox_from.name) || excluded?(mailbox_to.name)
|
113
|
+
|
88
114
|
@log.info "copying messages from #{imap_from.host}/#{mailbox_from.name} to #{imap_to.host}/#{mailbox_to.name}"
|
89
115
|
|
90
116
|
imap_from.connect
|
@@ -118,6 +144,49 @@ module Larch
|
|
118
144
|
end
|
119
145
|
end
|
120
146
|
end
|
147
|
+
|
148
|
+
def excluded?(name)
|
149
|
+
name = name.downcase
|
150
|
+
|
151
|
+
@exclude.each do |e|
|
152
|
+
return true if (e.is_a?(Regexp) ? !!(name =~ e) : File.fnmatch?(e, name))
|
153
|
+
end
|
154
|
+
|
155
|
+
return false
|
156
|
+
end
|
157
|
+
|
158
|
+
def glob_to_regex(str)
|
159
|
+
str.gsub!(/(.)/) {|c| GLOB_PATTERNS[$1] || Regexp.escape(c) }
|
160
|
+
Regexp.new("^#{str}$", Regexp::IGNORECASE)
|
161
|
+
end
|
162
|
+
|
163
|
+
def load_exclude_file(filename)
|
164
|
+
@exclude ||= []
|
165
|
+
lineno = 0
|
166
|
+
|
167
|
+
File.open(filename, 'rb') do |f|
|
168
|
+
f.each do |line|
|
169
|
+
lineno += 1
|
170
|
+
|
171
|
+
# Strip comments.
|
172
|
+
line.sub!(EXCLUDE_COMMENT, '')
|
173
|
+
line.strip!
|
174
|
+
|
175
|
+
# Skip empty lines.
|
176
|
+
next if line.empty?
|
177
|
+
|
178
|
+
if line =~ EXCLUDE_REGEX
|
179
|
+
@exclude << Regexp.new($1, Regexp::IGNORECASE)
|
180
|
+
else
|
181
|
+
@exclude << glob_to_regex(line)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
rescue => e
|
187
|
+
raise Larch::IMAP::FatalError, "error in exclude file at line #{lineno}: #{e}"
|
188
|
+
end
|
189
|
+
|
121
190
|
end
|
122
191
|
|
123
|
-
end
|
192
|
+
end
|