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 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*:: 1.0.0 (2009-03-17)
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, specify a URI beginning with <tt>imaps://</tt>:
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 source and
45
- destination folders using <tt>--from-folder</tt> and <tt>--to-folder</tt>:
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
- By default Larch will create the specified folder if it doesn't already exist on
51
- the destination server. To prevent this, add the <tt>--no-create-folder</tt>
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
- --from-folder "Sent Mail" --to-folder "Sent Mail" --no-create-folder
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 cause a
74
- deadlock to occur if a connection drops unexpectedly (either due to network
75
- issues or because the server closed the connection without warning) when the
76
- server has already begun sending a response and Net::IMAP is waiting to receive
77
- more data.
78
-
79
- If this happens, Net::IMAP will continue waiting forever without passing control
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 option overrides folders specified in URIs
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(options[:verbosity])
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
@@ -1,6 +1,5 @@
1
1
  module Larch
2
2
  class Error < StandardError; end
3
- class WatchdogException < Exception; end
4
3
 
5
4
  class IMAP
6
5
  class Error < Larch::Error; end
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 +:create_mailbox+ option is +false+, or if
123
- # +:create_mailbox+ is +true+ and mailbox creation fails, a
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 up to 3 times if a
164
- # recoverable error occurs, die if an unrecoverable error occurs.
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
@@ -1,6 +1,6 @@
1
1
  module Larch
2
2
  APP_NAME = 'Larch'
3
- APP_VERSION = '1.0.0.12'
3
+ APP_VERSION = '1.0.0.13'
4
4
  APP_AUTHOR = 'Ryan Grove'
5
5
  APP_EMAIL = 'ryan@wonko.com'
6
6
  APP_URL = 'http://github.com/rgrove/larch/'
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
- def init(log_level = :info)
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
- @copied = 0
38
- @failed = 0
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
- summary
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
- copy_messages(imap_from, imap_from.mailbox(imap_from.uri_mailbox || 'INBOX'),
64
- imap_to, imap_to.mailbox(imap_to.uri_mailbox || 'INBOX'))
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rgrove-larch
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.12
4
+ version: 1.0.0.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Grove