rgrove-larch 1.0.0.12 → 1.0.0.13
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 +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
|