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 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