rgrove-larch 1.0.0.9 → 1.0.0.10

Sign up to get free protection for your applications and to get access to all the features.
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