rgrove-larch 0.0.1.3 → 0.0.1.4

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.
Files changed (4) hide show
  1. data/bin/larch +31 -12
  2. data/lib/larch/imap.rb +8 -3
  3. data/lib/larch.rb +33 -14
  4. metadata +1 -1
data/bin/larch CHANGED
@@ -27,21 +27,25 @@ EOS
27
27
  # opt :dry_run, "Don't actually do anything.", :short => '-n'
28
28
  opt :fast_scan, "Use a faster (but less accurate) method to scan mailboxes. This may result in messages being re-copied.", :short => :none
29
29
  opt :from_folder, "Source folder to copy from.", :short => :none, :default => 'INBOX'
30
- opt :from_pass, "Source server password (will prompt if not specified).", :short => :none, :type => :string
31
- opt :from_user, "Source server username (will prompt if not specified).", :short => :none, :type => :string
30
+ opt :from_pass, "Source server password (Default: prompt).", :short => :none, :type => :string
31
+ opt :from_user, "Source server username (Default: prompt).", :short => :none, :type => :string
32
32
  opt :no_create_folder, "Don't create destination folders that don't already exist.", :short => :none
33
33
  opt :to_folder, "Destination folder to copy to.", :short => :none, :default => 'INBOX'
34
- opt :to_pass, "Destination server password (will prompt if not specified).", :short => :none, :type => :string
35
- opt :to_user, "Destination server username (will prompt if not specified).", :short => :none, :type => :string
34
+ opt :to_pass, "Destination server password (Default: prompt).", :short => :none, :type => :string
35
+ opt :to_user, "Destination server username (Default: prompt).", :short => :none, :type => :string
36
36
  opt :verbosity, "Output verbosity: debug, info, warn, error, or fatal.", :short => '-V', :default => 'info'
37
37
  end
38
38
 
39
39
  # Validate command-line options.
40
40
  [:from, :to].each do |sym|
41
- Trollop.die sym, "must be a valid IMAP URI (e.g. imap://example.com)" unless options[sym] =~ IMAP::REGEX_URI
41
+ unless options[sym] =~ IMAP::REGEX_URI
42
+ Trollop.die sym, "must be a valid IMAP URI (e.g. imap://example.com)"
43
+ end
42
44
  end
43
45
 
44
- Trollop.die :verbosity, "must be one of: #{Logger::LEVELS.keys.join(', ')}" unless Logger::LEVELS.has_key?(options[:verbosity].to_sym)
46
+ unless Logger::LEVELS.has_key?(options[:verbosity].to_sym)
47
+ Trollop.die :verbosity, "must be one of: #{Logger::LEVELS.keys.join(', ')}"
48
+ end
45
49
 
46
50
  # Create URIs.
47
51
  options[:from] = URI(options[:from])
@@ -50,16 +54,31 @@ EOS
50
54
  options[:to].path = '/' + CGI.escape(options[:to_folder].gsub(/^\//, ''))
51
55
 
52
56
  # Prompt for usernames and passwords if necessary.
53
- options[:from_user] = ask("Source username (#{options[:from].host}): ") unless options[:from_user]
54
- options[:from_pass] = ask("Source password (#{options[:from].host}): ") {|q| q.echo = false } unless options[:from_pass]
55
- options[:to_user] = ask("Destination username (#{options[:to].host}): ") unless options[:to_user]
56
- options[:to_pass] = ask("Destination password (#{options[:to].host}): ") {|q| q.echo = false } unless options[:to_pass]
57
+ unless options[:from_user]
58
+ options[:from_user] = ask("Source username (#{options[:from].host}): ")
59
+ end
60
+
61
+ unless options[:from_pass]
62
+ options[:from_pass] = ask("Source password (#{options[:from].host}): ") {|q| q.echo = false }
63
+ end
64
+
65
+ unless options[:to_user]
66
+ options[:to_user] = ask("Destination username (#{options[:to].host}): ")
67
+ end
68
+
69
+ unless options[:to_pass]
70
+ options[:to_pass] = ask("Destination password (#{options[:to].host}): ") {|q| q.echo = false }
71
+ end
57
72
 
58
73
  # Go go go!
59
74
  init(options[:verbosity])
60
75
 
61
- source = IMAP.new(options[:from], options[:from_user], options[:from_pass], :fast_scan => options[:fast_scan])
62
- dest = IMAP.new(options[:to], options[:to_user], options[:to_pass], :create_mailbox => !options[:no_create_folder], :fast_scan => options[:fast_scan])
76
+ source = IMAP.new(options[:from], options[:from_user], options[:from_pass],
77
+ :fast_scan => options[:fast_scan])
78
+
79
+ dest = IMAP.new(options[:to], options[:to_user], options[:to_pass],
80
+ :create_mailbox => !options[:no_create_folder],
81
+ :fast_scan => options[:fast_scan])
63
82
 
64
83
  for sig in [:SIGINT, :SIGQUIT, :SIGTERM]
65
84
  trap(sig) { @log.fatal "Interrupted (#{sig})"; summary; exit }
data/lib/larch/imap.rb CHANGED
@@ -145,7 +145,7 @@ class IMAP
145
145
  raise NotFoundError, "message not found: #{message_id}" if uid.nil?
146
146
 
147
147
  debug "fetching envelope: #{message_id}"
148
- safely { imap_uid_fetch([uid], 'ENVELOPE').first.attr['ENVELOPE'] }
148
+ imap_uid_fetch([uid], 'ENVELOPE').first.attr['ENVELOPE']
149
149
  end
150
150
 
151
151
  # Fetches a Larch::Message instance representing the message with the
@@ -157,7 +157,7 @@ class IMAP
157
157
  raise NotFoundError, "message not found: #{message_id}" if uid.nil?
158
158
 
159
159
  debug "#{peek ? 'peeking at' : 'fetching'} message: #{message_id}"
160
- data = safely { imap_uid_fetch([uid], [(peek ? 'BODY.PEEK[]' : 'BODY[]'), 'FLAGS', 'INTERNALDATE', 'ENVELOPE']) }.first
160
+ data = imap_uid_fetch([uid], [(peek ? 'BODY.PEEK[]' : 'BODY[]'), 'FLAGS', 'INTERNALDATE', 'ENVELOPE']).first
161
161
 
162
162
  Message.new(message_id, data.attr['ENVELOPE'], data.attr['BODY[]'],
163
163
  data.attr['FLAGS'], Time.parse(data.attr['INTERNALDATE']))
@@ -332,6 +332,7 @@ class IMAP
332
332
  begin
333
333
  unsafe_connect unless @imap
334
334
  rescue *RECOVERABLE_ERRORS => e
335
+ info "#{e.class.name}: #{e.message} (will retry)"
335
336
  raise unless (retries += 1) <= 3
336
337
 
337
338
  @imap = nil
@@ -344,14 +345,18 @@ class IMAP
344
345
  begin
345
346
  yield
346
347
  rescue *RECOVERABLE_ERRORS => e
348
+ info "#{e.class.name}: #{e.message} (will retry)"
347
349
  raise unless (retries += 1) <= 3
348
350
 
349
351
  sleep 2 * retries
350
352
  retry
351
353
  end
352
354
 
355
+ rescue Net::IMAP::NoResponseError => e
356
+ raise Error, "#{e.class.name}: #{e.message} (giving up)"
357
+
353
358
  rescue IOError, Net::IMAP::Error, OpenSSL::SSL::SSLError, SocketError, SystemCallError => e
354
- raise FatalError, "while communicating with IMAP server (#{e.class.name}): #{e.message}"
359
+ raise FatalError, "#{e.class.name}: #{e.message} (giving up)"
355
360
  end
356
361
 
357
362
  def unsafe_connect
data/lib/larch.rb CHANGED
@@ -34,7 +34,8 @@ module Larch
34
34
  raise ArgumentError, "source must be a Larch::IMAP instance" unless source.is_a?(IMAP)
35
35
  raise ArgumentError, "dest must be a Larch::IMAP instance" unless dest.is_a?(IMAP)
36
36
 
37
- msgq = SizedQueue.new(32)
37
+ msgq = SizedQueue.new(32)
38
+ mutex = Mutex.new
38
39
 
39
40
  @copied = 0
40
41
  @failed = 0
@@ -42,18 +43,30 @@ module Larch
42
43
 
43
44
  @log.info "copying messages from #{source.uri} to #{dest.uri}"
44
45
 
45
- # Note that the stats variables are being accessed without synchronization
46
- # in the following threads. This is currently safe because the threads
47
- # never access the same variables. If we end up adding additional threads,
48
- # these accesses need to be synchronized.
46
+ source_scan = Thread.new do
47
+ source.scan_mailbox
48
+ end
49
+
50
+ dest_scan = Thread.new do
51
+ dest.scan_mailbox
52
+ end
49
53
 
50
- source_thread = Thread.new do
54
+ source_scan.join
55
+ dest_scan.join
56
+
57
+ source_copy = Thread.new do
51
58
  begin
52
- @total = source.length
59
+ mutex.synchronize { @total = source.length }
53
60
 
54
61
  source.each do |id|
55
62
  next if dest.has_message?(id)
56
- msgq << source.peek(id)
63
+
64
+ begin
65
+ msgq << source.peek(id)
66
+ rescue Larch::IMAP::Error => e
67
+ mutex.synchronize { @failed += 1 }
68
+ next
69
+ end
57
70
  end
58
71
 
59
72
  rescue => e
@@ -64,29 +77,35 @@ module Larch
64
77
  end
65
78
  end
66
79
 
67
- dest_thread = Thread.new do
80
+ dest_copy = Thread.new do
68
81
  begin
69
82
  while msg = msgq.pop do
70
83
  break if msg == :finished
71
84
 
72
- from = msg.envelope.from.first
73
- @log.info "copying message: #{from.mailbox}@#{from.host} - #{msg.envelope.subject}"
85
+ if msg.envelope.from
86
+ env_from = msg.envelope.from.first
87
+ from = "#{env_from.mailbox}@#{env_from.host}"
88
+ else
89
+ from = '?'
90
+ end
91
+
92
+ @log.info "copying message: #{from} - #{msg.envelope.subject}"
74
93
  dest << msg
75
94
 
76
- @copied += 1
95
+ mutex.synchronize { @copied += 1 }
77
96
  end
78
97
 
79
98
  rescue IMAP::FatalError => e
80
99
  @log.fatal e.message
81
100
 
82
101
  rescue => e
83
- @failed += 1
102
+ mutex.synchronize { @failed += 1 }
84
103
  @log.error e.message
85
104
  retry
86
105
  end
87
106
  end
88
107
 
89
- dest_thread.join
108
+ dest_copy.join
90
109
 
91
110
  source.disconnect
92
111
  dest.disconnect
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: 0.0.1.3
4
+ version: 0.0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Grove