rgrove-larch 1.0.0.9 → 1.0.0.10

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
@@ -5,7 +5,10 @@ Version 1.0.1 (?)
5
5
  * Ruby 1.9.1 support.
6
6
  * Much more robust handling of unexpected server disconnects and dropped
7
7
  connections.
8
- * Option to verify server SSL certificates.
8
+ * Add --all option to copy all folders recursively.
9
+ * Add --dry-run option to simulate changes without actually making them.
10
+ * Add --ssl-certs option to specify a bundle of trusted SSL certificates.
11
+ * Add --ssl-verify option to verify server SSL certificates.
9
12
  * Fix excessive post-scan processing times for very large mailboxes.
10
13
  * Fetch message headers in blocks of up to 1024 at a time rather than all at
11
14
  once, to prevent potential problems with certain servers when a mailbox
data/bin/larch CHANGED
@@ -23,6 +23,7 @@ EOS
23
23
  opt :to, "URI of the destination IMAP server.", :short => '-t', :type => :string, :required => true
24
24
 
25
25
  text "\nCopy Options:"
26
+ opt :all, "Copy all folders recursively", :short => :none
26
27
  opt :from_folder, "Source folder to copy from", :short => :none, :default => 'INBOX'
27
28
  opt :from_pass, "Source server password (default: prompt)", :short => :none, :type => :string
28
29
  opt :from_user, "Source server username (default: prompt)", :short => :none, :type => :string
@@ -31,7 +32,7 @@ EOS
31
32
  opt :to_user, "Destination server username (default: prompt)", :short => :none, :type => :string
32
33
 
33
34
  text "\nGeneral Options:"
34
- # opt :dry_run, "Don't actually do anything.", :short => '-n'
35
+ opt :dry_run, "Don't actually make any changes.", :short => '-n'
35
36
  opt :fast_scan, "Use a faster (but less accurate) method to scan mailboxes. This may result in messages being re-copied.", :short => :none
36
37
  opt :max_retries, "Maximum number of times to retry after a recoverable error", :short => :none, :default => 3
37
38
  opt :no_create_folder, "Don't create destination folders that don't already exist", :short => :none
@@ -47,6 +48,10 @@ EOS
47
48
  end
48
49
  end
49
50
 
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
+
50
55
  unless Logger::LEVELS.has_key?(options[:verbosity].to_sym)
51
56
  Trollop.die :verbosity, "must be one of: #{Logger::LEVELS.keys.join(', ')}"
52
57
  end
@@ -55,6 +60,13 @@ EOS
55
60
  uri_from = URI(options[:from])
56
61
  uri_to = URI(options[:to])
57
62
 
63
+ # --all option overrides folders specified in URIs
64
+ if options[:all_given]
65
+ uri_from.path = ''
66
+ uri_to.path = ''
67
+ end
68
+
69
+ # --from-folder and --to-folder options override folders specified in URIs
58
70
  if options[:from_folder_given] || options[:to_folder_given]
59
71
  uri_from.path = '/' + CGI.escape(options[:from_folder].gsub(/^\//, ''))
60
72
  uri_to.path = '/' + CGI.escape(options[:to_folder].gsub(/^\//, ''))
@@ -78,6 +90,7 @@ EOS
78
90
  Net::IMAP.debug = true if @log.level == :insane
79
91
 
80
92
  imap_from = Larch::IMAP.new(uri_from,
93
+ :dry_run => options[:dry_run],
81
94
  :fast_scan => options[:fast_scan],
82
95
  :max_retries => options[:max_retries],
83
96
  :ssl_certs => options[:ssl_certs] || nil,
@@ -85,7 +98,8 @@ EOS
85
98
  )
86
99
 
87
100
  imap_to = Larch::IMAP.new(uri_to,
88
- :create_mailbox => !options[:no_create_folder],
101
+ :create_mailbox => !options[:no_create_folder] && !options[:dry_run],
102
+ :dry_run => options[:dry_run],
89
103
  :fast_scan => options[:fast_scan],
90
104
  :max_retries => options[:max_retries],
91
105
  :ssl_certs => options[:ssl_certs] || nil,
@@ -101,5 +115,9 @@ EOS
101
115
  end
102
116
  end
103
117
 
104
- copy_folder(imap_from, imap_to)
118
+ if options[:all_given]
119
+ copy_all(imap_from, imap_to)
120
+ else
121
+ copy_folder(imap_from, imap_to)
122
+ end
105
123
  end
@@ -13,13 +13,14 @@ class Mailbox
13
13
  # Minimum time (in seconds) allowed between mailbox scans.
14
14
  SCAN_INTERVAL = 60
15
15
 
16
- def initialize(imap, name, delim, *attr)
16
+ def initialize(imap, name, delim, subscribed, *attr)
17
17
  raise ArgumentError, "must provide a Larch::IMAP instance" unless imap.is_a?(Larch::IMAP)
18
18
 
19
- @delim = delim
20
- @imap = imap
21
- @name = name
22
- @attr = attr
19
+ @imap = imap
20
+ @name = name
21
+ @delim = delim
22
+ @subscribed = subscribed
23
+ @attr = attr
23
24
 
24
25
  @ids = {}
25
26
  @last_id = 0
@@ -54,7 +55,7 @@ class Mailbox
54
55
  imap_select(!!@imap.options[:create_mailbox])
55
56
 
56
57
  debug "appending message: #{message.id}"
57
- @imap.conn.append(@name, message.rfc822, message.flags, message.internaldate)
58
+ @imap.conn.append(@name, message.rfc822, message.flags, message.internaldate) unless @imap.options[:dry_run]
58
59
  end
59
60
 
60
61
  true
@@ -161,6 +162,25 @@ class Mailbox
161
162
  end
162
163
  end
163
164
 
165
+ # Subscribes to this mailbox.
166
+ def subscribe(force = false)
167
+ return if subscribed? && !force
168
+ @imap.safely { @imap.conn.subscribe(@name) }
169
+ @mutex.synchronize { @subscribed = true }
170
+ end
171
+
172
+ # Returns +true+ if this mailbox is subscribed, +false+ otherwise.
173
+ def subscribed?
174
+ @subscribed
175
+ end
176
+
177
+ # Unsubscribes to this mailbox.
178
+ def unsubscribe(force = false)
179
+ return unless subscribed? || force
180
+ @imap.safely { @imap.conn.unsubscribe(@name) }
181
+ @mutex.synchronize { @subscribed = false }
182
+ end
183
+
164
184
  private
165
185
 
166
186
  # Creates an id suitable for uniquely identifying a specific message across
@@ -240,10 +260,10 @@ class Mailbox
240
260
  rescue Net::IMAP::NoResponseError => e
241
261
  raise Error, "unable to select mailbox: #{e.message}" unless create
242
262
 
243
- info "creating mailbox: #{mailbox}"
263
+ info "creating mailbox: #{@name}"
244
264
 
245
265
  begin
246
- @imap.conn.create(@name)
266
+ @imap.conn.create(@name) unless @imap.options[:dry_run]
247
267
  retry
248
268
  rescue => e
249
269
  raise Error, "unable to create mailbox: #{e.message}"
data/lib/larch/imap.rb CHANGED
@@ -24,6 +24,12 @@ class IMAP
24
24
  # If +true+, mailboxes that don't already exist will be created if
25
25
  # necessary.
26
26
  #
27
+ # [:dry_run]
28
+ # If +true+, read-only operations will be performed as usual and all change
29
+ # operations will be simulated, but no changes will actually be made. Note
30
+ # that it's not actually possible to simulation mailbox creation, so
31
+ # +:dry_run+ mode always behaves as if +:create_mailbox+ is +false+.
32
+ #
27
33
  # [:fast_scan]
28
34
  # If +true+, a faster but less accurate method will be used to scan
29
35
  # mailboxes. This will speed up the initial mailbox scan, but will also
@@ -125,7 +131,7 @@ class IMAP
125
131
  raise unless @options[:create_mailbox] && retries == 0
126
132
 
127
133
  info "creating mailbox: #{name}"
128
- safely { @conn.create(name) }
134
+ safely { @conn.create(name) } unless @options[:dry_run]
129
135
 
130
136
  retries += 1
131
137
  retry
@@ -306,15 +312,17 @@ class IMAP
306
312
  end
307
313
 
308
314
  def update_mailboxes
309
- list = safely { @conn.list('', '*') }
315
+ all = safely { @conn.list('', '*') }
316
+ subscribed = safely { @conn.lsub('', '*') }
310
317
 
311
318
  @mutex.synchronize do
312
319
  # Remove cached mailboxes that no longer exist.
313
- @mailboxes.delete_if {|k, v| !list.any?{|mb| mb.name == k}}
320
+ @mailboxes.delete_if {|k, v| !all.any?{|mb| mb.name == k}}
314
321
 
315
322
  # Update cached mailboxes.
316
- list.each do |mb|
317
- @mailboxes[mb.name] ||= Mailbox.new(self, mb.name, mb.delim, mb.attr)
323
+ all.each do |mb|
324
+ @mailboxes[mb.name] ||= Mailbox.new(self, mb.name, mb.delim,
325
+ subscribed.any?{|s| s.name == mb.name}, mb.attr)
318
326
  end
319
327
  end
320
328
  end
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.7'
3
+ APP_VERSION = '1.0.0.10'
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
@@ -28,26 +28,70 @@ module Larch
28
28
  @total = 0
29
29
  end
30
30
 
31
+ # Recursively copies all messages in all folders from the source to the
32
+ # destination.
33
+ def copy_all(imap_from, imap_to)
34
+ raise ArgumentError, "imap_from must be a Larch::IMAP instance" unless imap_from.is_a?(IMAP)
35
+ raise ArgumentError, "imap_to must be a Larch::IMAP instance" unless imap_to.is_a?(IMAP)
36
+
37
+ imap_from.each_mailbox do |mailbox_from|
38
+ @copied = 0
39
+ @failed = 0
40
+ @total = 0
41
+
42
+ mailbox_to = imap_to.mailbox(mailbox_from.name)
43
+ copy_messages(imap_from, mailbox_from, imap_to, mailbox_to)
44
+ mailbox_to.subscribe if mailbox_from.subscribed?
45
+
46
+ summary
47
+ end
48
+
49
+ rescue => e
50
+ @log.fatal e.message
51
+ summary
52
+ end
53
+
54
+ # Copies the messages in a single IMAP folder (non-recursively) from the
55
+ # source to the destination.
31
56
  def copy_folder(imap_from, imap_to)
32
- raise ArgumentError, "source must be a Larch::IMAP instance" unless imap_from.is_a?(IMAP)
33
- raise ArgumentError, "dest must be a Larch::IMAP instance" unless imap_to.is_a?(IMAP)
57
+ raise ArgumentError, "imap_from must be a Larch::IMAP instance" unless imap_from.is_a?(IMAP)
58
+ raise ArgumentError, "imap_to must be a Larch::IMAP instance" unless imap_to.is_a?(IMAP)
34
59
 
35
60
  @copied = 0
36
61
  @failed = 0
37
62
  @total = 0
38
63
 
39
- mailbox_from_name = imap_from.uri_mailbox || 'INBOX'
40
- mailbox_to_name = imap_to.uri_mailbox || 'INBOX'
64
+ copy_messages(imap_from, imap_from.mailbox(imap_from.uri_mailbox || 'INBOX'),
65
+ imap_to, imap_to.mailbox(imap_to.uri_mailbox || 'INBOX'))
41
66
 
42
- @log.info "copying messages from #{imap_from.host}/#{mailbox_from_name} to #{imap_to.host}/#{mailbox_to_name}"
67
+ imap_from.disconnect
68
+ imap_to.disconnect
69
+
70
+ rescue => e
71
+ @log.fatal e.message
72
+
73
+ ensure
74
+ summary
75
+ end
76
+
77
+ def summary
78
+ @log.info "#{@copied} message(s) copied, #{@failed} failed, #{@total - @copied - @failed} untouched out of #{@total} total"
79
+ end
80
+
81
+ private
82
+
83
+ def copy_messages(imap_from, mailbox_from, imap_to, mailbox_to)
84
+ raise ArgumentError, "imap_from must be a Larch::IMAP instance" unless imap_from.is_a?(IMAP)
85
+ raise ArgumentError, "mailbox_from must be a Larch::IMAP::Mailbox instance" unless mailbox_from.is_a?(IMAP::Mailbox)
86
+ raise ArgumentError, "imap_to must be a Larch::IMAP instance" unless imap_to.is_a?(IMAP)
87
+ raise ArgumentError, "mailbox_to must be a Larch::IMAP::Mailbox instance" unless mailbox_to.is_a?(IMAP::Mailbox)
88
+
89
+ @log.info "copying messages from #{imap_from.host}/#{mailbox_from.name} to #{imap_to.host}/#{mailbox_to.name}"
43
90
 
44
91
  imap_from.connect
45
92
  imap_to.connect
46
93
 
47
- mailbox_from = imap_from.mailbox(mailbox_from_name)
48
- mailbox_to = imap_to.mailbox(mailbox_to_name)
49
-
50
- @total = mailbox_from.length
94
+ @total += mailbox_from.length
51
95
 
52
96
  mailbox_from.each do |id|
53
97
  next if mailbox_to.has_message?(id)
@@ -74,19 +118,6 @@ module Larch
74
118
  next
75
119
  end
76
120
  end
77
-
78
- imap_from.disconnect
79
- imap_to.disconnect
80
-
81
- rescue => e
82
- @log.fatal e.message
83
-
84
- ensure
85
- summary
86
- end
87
-
88
- def summary
89
- @log.info "#{@copied} message(s) copied, #{@failed} failed, #{@total - @copied - @failed} untouched out of #{@total} total"
90
121
  end
91
122
  end
92
123
 
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.9
4
+ version: 1.0.0.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Grove