rgrove-larch 1.0.0.7 → 1.0.0.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. data/bin/larch +27 -27
  2. data/lib/larch.rb +29 -101
  3. data/lib/larch/errors.rb +2 -1
  4. data/lib/larch/imap.rb +112 -299
  5. metadata +2 -2
data/bin/larch CHANGED
@@ -24,18 +24,18 @@ EOS
24
24
 
25
25
  text "\nCopy Options:"
26
26
  opt :from_folder, "Source folder to copy from", :short => :none, :default => 'INBOX'
27
- opt :from_pass, "Source server password (Default: prompt)", :short => :none, :type => :string
28
- opt :from_user, "Source server username (Default: prompt)", :short => :none, :type => :string
27
+ opt :from_pass, "Source server password (default: prompt)", :short => :none, :type => :string
28
+ opt :from_user, "Source server username (default: prompt)", :short => :none, :type => :string
29
29
  opt :to_folder, "Destination folder to copy to", :short => :none, :default => 'INBOX'
30
- opt :to_pass, "Destination server password (Default: prompt)", :short => :none, :type => :string
31
- opt :to_user, "Destination server username (Default: prompt)", :short => :none, :type => :string
30
+ opt :to_pass, "Destination server password (default: prompt)", :short => :none, :type => :string
31
+ opt :to_user, "Destination server username (default: prompt)", :short => :none, :type => :string
32
32
 
33
33
  text "\nGeneral Options:"
34
34
  # opt :dry_run, "Don't actually do anything.", :short => '-n'
35
35
  opt :fast_scan, "Use a faster (but less accurate) method to scan mailboxes. This may result in messages being re-copied.", :short => :none
36
36
  opt :max_retries, "Maximum number of times to retry after a recoverable error", :short => :none, :default => 3
37
37
  opt :no_create_folder, "Don't create destination folders that don't already exist", :short => :none
38
- opt :ssl_certs, "Path to a trusted certificate bundle to use to verify server SSL certificates", :short => :none
38
+ opt :ssl_certs, "Path to a trusted certificate bundle to use to verify server SSL certificates", :short => :none, :type => :string
39
39
  opt :ssl_verify, "Verify server SSL certificates", :short => :none
40
40
  opt :verbosity, "Output verbosity: debug, info, warn, error, or fatal", :short => '-V', :default => 'info'
41
41
  end
@@ -52,45 +52,45 @@ EOS
52
52
  end
53
53
 
54
54
  # Create URIs.
55
- options[:from] = URI(options[:from])
56
- options[:to] = URI(options[:to])
57
- options[:from].path = '/' + CGI.escape(options[:from_folder].gsub(/^\//, ''))
58
- options[:to].path = '/' + CGI.escape(options[:to_folder].gsub(/^\//, ''))
59
-
60
- # Prompt for usernames and passwords if necessary.
61
- unless options[:from_user]
62
- options[:from_user] = ask("Source username (#{options[:from].host}): ")
63
- end
55
+ uri_from = URI(options[:from])
56
+ uri_to = URI(options[:to])
64
57
 
65
- unless options[:from_pass]
66
- options[:from_pass] = ask("Source password (#{options[:from].host}): ") {|q| q.echo = false }
58
+ if options[:from_folder_given] || options[:to_folder_given]
59
+ uri_from.path = '/' + CGI.escape(options[:from_folder].gsub(/^\//, ''))
60
+ uri_to.path = '/' + CGI.escape(options[:to_folder].gsub(/^\//, ''))
67
61
  end
68
62
 
69
- unless options[:to_user]
70
- options[:to_user] = ask("Destination username (#{options[:to].host}): ")
71
- end
63
+ # Usernames and passwords specified as arguments override those in the URIs
64
+ uri_from.user = CGI.escape(options[:from_user]) if options[:from_user]
65
+ uri_from.password = CGI.escape(options[:from_pass]) if options[:from_pass]
66
+ uri_to.user = CGI.escape(options[:to_user]) if options[:to_user]
67
+ uri_to.password = CGI.escape(options[:to_pass]) if options[:to_pass]
72
68
 
73
- unless options[:to_pass]
74
- options[:to_pass] = ask("Destination password (#{options[:to].host}): ") {|q| q.echo = false }
75
- end
69
+ # If usernames/passwords aren't specified in either URIs or args, then prompt.
70
+ uri_from.user ||= CGI.escape(ask("Source username (#{uri_from.host}): "))
71
+ uri_from.password ||= CGI.escape(ask("Source password (#{uri_from.host}): ") {|q| q.echo = false })
72
+ uri_to.user ||= CGI.escape(ask("Destination username (#{uri_to.host}): "))
73
+ uri_to.password ||= CGI.escape(ask("Destination password (#{uri_to.host}): ") {|q| q.echo = false })
76
74
 
77
75
  # Go go go!
78
76
  init(options[:verbosity])
79
77
 
80
78
  Net::IMAP.debug = true if @log.level == :insane
81
79
 
82
- source = IMAP.new(options[:from], options[:from_user], options[:from_pass],
80
+ imap_from = Larch::IMAP.new(uri_from,
83
81
  :fast_scan => options[:fast_scan],
84
82
  :max_retries => options[:max_retries],
85
83
  :ssl_certs => options[:ssl_certs] || nil,
86
- :ssl_verify => options[:ssl_verify])
84
+ :ssl_verify => options[:ssl_verify]
85
+ )
87
86
 
88
- dest = IMAP.new(options[:to], options[:to_user], options[:to_pass],
87
+ imap_to = Larch::IMAP.new(uri_to,
89
88
  :create_mailbox => !options[:no_create_folder],
90
89
  :fast_scan => options[:fast_scan],
91
90
  :max_retries => options[:max_retries],
92
91
  :ssl_certs => options[:ssl_certs] || nil,
93
- :ssl_verify => options[:ssl_verify])
92
+ :ssl_verify => options[:ssl_verify]
93
+ )
94
94
 
95
95
  unless RUBY_PLATFORM =~ /mswin|mingw|bccwin|wince|java/
96
96
  begin
@@ -101,5 +101,5 @@ EOS
101
101
  end
102
102
  end
103
103
 
104
- copy(source, dest)
104
+ copy_folder(imap_from, imap_to)
105
105
  end
data/lib/larch.rb CHANGED
@@ -11,6 +11,7 @@ require 'uri'
11
11
 
12
12
  require 'larch/errors'
13
13
  require 'larch/imap'
14
+ require 'larch/imap/mailbox'
14
15
  require 'larch/logger'
15
16
  require 'larch/version'
16
17
 
@@ -27,128 +28,55 @@ module Larch
27
28
  @total = 0
28
29
  end
29
30
 
30
- # Copies messages from _source_ to _dest_ if they don't already exist in
31
- # _dest_. Both _source_ and _dest_ must be instances of Larch::IMAP.
32
- def copy(source, dest)
33
- raise ArgumentError, "source must be a Larch::IMAP instance" unless source.is_a?(IMAP)
34
- raise ArgumentError, "dest must be a Larch::IMAP instance" unless dest.is_a?(IMAP)
35
-
36
- msgq = SizedQueue.new(8)
37
- mutex = Mutex.new
31
+ 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)
38
34
 
39
35
  @copied = 0
40
36
  @failed = 0
41
37
  @total = 0
42
38
 
43
- @log.info "copying messages from #{source.uri} to #{dest.uri}"
39
+ mailbox_from_name = imap_from.uri_mailbox || 'INBOX'
40
+ mailbox_to_name = imap_to.uri_mailbox || 'INBOX'
44
41
 
45
- source.connect
46
- dest.connect
42
+ @log.info "copying messages from #{imap_from.host}/#{mailbox_from_name} to #{imap_to.host}/#{mailbox_to_name}"
47
43
 
48
- source_thread = Thread.new do
49
- begin
50
- source.scan_mailbox
51
- mutex.synchronize { @total = source.length }
52
-
53
- source.each do |id|
54
- next if dest.has_message?(id)
55
-
56
- begin
57
- Thread.current[:fetching] = true
58
- msgq << source.peek(id)
59
- Thread.current[:fetching] = false
60
-
61
- rescue Larch::IMAP::Error => e
62
- # TODO: Keep failed message envelopes in a buffer for later output?
63
- mutex.synchronize { @failed += 1 }
64
- @log.error e.message
65
- next
66
- end
67
- end
44
+ imap_from.connect
45
+ imap_to.connect
68
46
 
69
- rescue Larch::WatchdogException => e
70
- Thread.current[:fetching] = false
71
- @log.debug "#{source.username}@#{source.host}: watchdog exception"
72
- source.noop
73
- retry
47
+ mailbox_from = imap_from.mailbox(mailbox_from_name)
48
+ mailbox_to = imap_to.mailbox(mailbox_to_name)
74
49
 
75
- rescue => e
76
- @log.fatal "#{source.username}@#{source.host}: #{e.class.name}: #{e.message}"
77
- Kernel.abort
50
+ @total = mailbox_from.length
78
51
 
79
- ensure
80
- msgq << :finished
81
- end
82
- end
52
+ mailbox_from.each do |id|
53
+ next if mailbox_to.has_message?(id)
83
54
 
84
- dest_thread = Thread.new do
85
55
  begin
86
- dest.scan_mailbox
56
+ msg = mailbox_from.peek(id)
87
57
 
88
- while msg = msgq.pop do
89
- break if msg == :finished
90
-
91
- if msg.envelope.from
92
- env_from = msg.envelope.from.first
93
- from = "#{env_from.mailbox}@#{env_from.host}"
94
- else
95
- from = '?'
96
- end
58
+ if msg.envelope.from
59
+ env_from = msg.envelope.from.first
60
+ from = "#{env_from.mailbox}@#{env_from.host}"
61
+ else
62
+ from = '?'
63
+ end
97
64
 
98
- @log.info "copying message: #{from} - #{msg.envelope.subject}"
65
+ @log.info "copying message: #{from} - #{msg.envelope.subject}"
99
66
 
100
- Thread.current[:last_id] = msg.id
101
- dest << msg
102
-
103
- mutex.synchronize { @copied += 1 }
104
- end
67
+ mailbox_to << msg
68
+ @copied += 1
105
69
 
106
70
  rescue Larch::IMAP::Error => e
107
- mutex.synchronize { @failed += 1 }
71
+ # TODO: Keep failed message envelopes in a buffer for later output?
72
+ @failed += 1
108
73
  @log.error e.message
109
- retry
110
-
111
- rescue Larch::WatchdogException => e
112
- Thread.current[:last_id] = nil
113
- @log.debug "#{dest.username}@#{dest.host}: watchdog exception"
114
- dest.noop
115
- retry
116
-
117
- rescue => e
118
- @log.fatal "#{dest.username}@#{dest.host}: #{e.class.name}: #{e.message}"
119
- Kernel.abort
120
- end
121
- end
122
-
123
- watchdog_thread = Thread.new do
124
- source_flags = 0
125
- dest_flags = 0
126
- dest_lastid = nil
127
-
128
- loop do
129
- sleep 10
130
-
131
- if msgq.length == 0 && source_thread[:fetching] && (source_flags += 1) > 1
132
- source_flags = 0
133
- source_thread.raise(WatchdogException)
134
- end
135
-
136
- if dest_thread[:last_id]
137
- if dest_lastid == dest_thread[:last_id] && (dest_flags += 1) > 2
138
- dest_flags = 0
139
- dest_lastid = nil
140
- dest_thread.raise(WatchdogException)
141
- else
142
- dest_lastid = dest_thread[:last_id]
143
- end
144
- end
74
+ next
145
75
  end
146
76
  end
147
77
 
148
- dest_thread.join
149
-
150
- source.disconnect
151
- dest.disconnect
78
+ imap_from.disconnect
79
+ imap_to.disconnect
152
80
 
153
81
  rescue => e
154
82
  @log.fatal e.message
data/lib/larch/errors.rb CHANGED
@@ -5,6 +5,7 @@ module Larch
5
5
  class IMAP
6
6
  class Error < Larch::Error; end
7
7
  class FatalError < Error; end
8
- class NotFoundError < Error; end
8
+ class MailboxNotFoundError < Error; end
9
+ class MessageNotFoundError < Error; end
9
10
  end
10
11
  end
data/lib/larch/imap.rb CHANGED
@@ -6,34 +6,23 @@ module Larch
6
6
  # required reading if you're doing anything with IMAP in Ruby:
7
7
  # http://sup.rubyforge.org
8
8
  class IMAP
9
- attr_reader :username
10
-
11
- # Maximum number of messages to fetch at once.
12
- MAX_FETCH_COUNT = 1024
13
-
14
- # Regex to capture the individual fields in an IMAP fetch command.
15
- REGEX_FIELDS = /([0-9A-Z\.]+\[[^\]]+\](?:<[0-9\.]+>)?|[0-9A-Z\.]+)/
16
-
17
- # Regex to capture a Message-Id header.
18
- REGEX_MESSAGE_ID = /message-id\s*:\s*(\S+)/i
9
+ attr_reader :conn, :options
19
10
 
20
11
  # URI format validation regex.
21
12
  REGEX_URI = URI.regexp(['imap', 'imaps'])
22
13
 
23
- # Minimum time (in seconds) allowed between mailbox scans.
24
- SCAN_INTERVAL = 60
25
-
26
14
  # Larch::IMAP::Message represents a transferable IMAP message which can be
27
15
  # passed between Larch::IMAP instances.
28
16
  Message = Struct.new(:id, :envelope, :rfc822, :flags, :internaldate)
29
17
 
30
18
  # Initializes a new Larch::IMAP instance that will connect to the specified
31
- # IMAP URI and authenticate using the specified _username_ and _password_.
19
+ # IMAP URI.
32
20
  #
33
- # The following options may also be specified:
21
+ # In addition to the URI, the following options may also be specified:
34
22
  #
35
23
  # [:create_mailbox]
36
- # If +true+, the specified mailbox will be created if necessary.
24
+ # If +true+, mailboxes that don't already exist will be created if
25
+ # necessary.
37
26
  #
38
27
  # [:fast_scan]
39
28
  # If +true+, a faster but less accurate method will be used to scan
@@ -58,32 +47,25 @@ class IMAP
58
47
  # certificate bundle specified in +ssl_certs+. By default, server SSL
59
48
  # certificates are not verified.
60
49
  #
61
- def initialize(uri, username, password, options = {})
50
+ def initialize(uri, options = {})
62
51
  raise ArgumentError, "not an IMAP URI: #{uri}" unless uri.is_a?(URI) || uri =~ REGEX_URI
63
- raise ArgumentError, "must provide a username and password" unless username && password
64
52
  raise ArgumentError, "options must be a Hash" unless options.is_a?(Hash)
65
53
 
66
- @uri = uri.is_a?(URI) ? uri : URI(uri)
67
- @username = username
68
- @password = password
69
- @options = {:max_retries => 3, :ssl_verify => false}.merge(options)
54
+ @options = {:max_retries => 3, :ssl_verify => false}.merge(options)
55
+ @uri = uri.is_a?(URI) ? uri : URI(uri)
70
56
 
71
- @ids = {}
72
- @imap = nil
73
- @last_id = 0
74
- @last_scan = nil
75
- @mutex = Mutex.new
57
+ raise ArgumentError, "must provide a username and password" unless @uri.user && @uri.password
76
58
 
77
- # Valid mailbox states are :closed (no mailbox open), :examined (mailbox
78
- # open and read-only), or :selected (mailbox open and read-write).
79
- @mailbox_state = :closed
59
+ @conn = nil
60
+ @mailboxes = {}
61
+ @mutex = Mutex.new
80
62
 
81
63
  # Create private convenience methods (debug, info, warn, etc.) to make
82
64
  # logging easier.
83
65
  Logger::LEVELS.each_key do |level|
84
66
  IMAP.class_eval do
85
67
  define_method(level) do |msg|
86
- Larch.log.log(level, "#{@username}@#{host}: #{msg}")
68
+ Larch.log.log(level, "#{username}@#{host}: #{msg}")
87
69
  end
88
70
 
89
71
  private level
@@ -91,37 +73,19 @@ class IMAP
91
73
  end
92
74
  end
93
75
 
94
- # Appends the specified Larch::IMAP::Message to this mailbox if it doesn't
95
- # already exist. Returns +true+ if the message was appended successfully,
96
- # +false+ if the message already exists in the mailbox.
97
- def append(message)
98
- raise ArgumentError, "must provide a Larch::IMAP::Message object" unless message.is_a?(Message)
99
- return false if has_message?(message)
100
-
101
- safely do
102
- imap_select(!!@options[:create_mailbox])
103
-
104
- debug "appending message: #{message.id}"
105
- @imap.append(mailbox, message.rfc822, message.flags, message.internaldate)
106
- end
107
-
108
- true
109
- end
110
- alias << append
111
-
112
76
  # Connects to the IMAP server and logs in if a connection hasn't already been
113
77
  # established.
114
78
  def connect
115
- return if @imap
79
+ return if @conn
116
80
  safely {} # connect, but do nothing else
117
81
  end
118
82
 
119
83
  # Closes the IMAP connection if one is currently open.
120
84
  def disconnect
121
- return unless @imap
85
+ return unless @conn
122
86
 
123
87
  begin
124
- @imap.disconnect
88
+ @conn.disconnect
125
89
  rescue Errno::ENOTCONN => e
126
90
  debug "#{e.class.name}: #{e.message}"
127
91
  end
@@ -131,47 +95,11 @@ class IMAP
131
95
  info "disconnected"
132
96
  end
133
97
 
134
- # Iterates through Larch message ids in this mailbox, yielding each one to the
135
- # provided block.
136
- def each
137
- scan_mailbox
138
- ids = @ids
139
-
140
- ids.each_key {|id| yield id }
141
- end
142
-
143
- # Gets a Net::IMAP::Envelope for the specified message id.
144
- def envelope(message_id)
145
- scan_mailbox
146
- uid = @ids[message_id]
147
-
148
- raise NotFoundError, "message not found: #{message_id}" if uid.nil?
149
-
150
- debug "fetching envelope: #{message_id}"
151
- imap_uid_fetch([uid], 'ENVELOPE').first.attr['ENVELOPE']
152
- end
153
-
154
- # Fetches a Larch::IMAP::Message struct representing the message with the
155
- # specified Larch message id.
156
- def fetch(message_id, peek = false)
157
- scan_mailbox
158
- uid = @ids[message_id]
159
-
160
- raise NotFoundError, "message not found: #{message_id}" if uid.nil?
161
-
162
- debug "#{peek ? 'peeking at' : 'fetching'} message: #{message_id}"
163
- data = imap_uid_fetch([uid], [(peek ? 'BODY.PEEK[]' : 'BODY[]'), 'FLAGS', 'INTERNALDATE', 'ENVELOPE']).first
164
-
165
- Message.new(message_id, data.attr['ENVELOPE'], data.attr['BODY[]'],
166
- data.attr['FLAGS'], Time.parse(data.attr['INTERNALDATE']))
167
- end
168
- alias [] fetch
169
-
170
- # Returns +true+ if a message with the specified Larch <em>message_id</em>
171
- # exists in this mailbox, +false+ otherwise.
172
- def has_message?(message_id)
173
- scan_mailbox
174
- @ids.has_key?(message_id)
98
+ # Iterates through all mailboxes in the account, yielding each one as a
99
+ # Larch::IMAP::Mailbox instance to the given block.
100
+ def each_mailbox
101
+ update_mailboxes
102
+ @mailboxes.each_value {|mailbox| yield mailbox }
175
103
  end
176
104
 
177
105
  # Gets the IMAP hostname.
@@ -179,26 +107,39 @@ class IMAP
179
107
  @uri.host
180
108
  end
181
109
 
182
- # Gets the number of messages in this mailbox.
183
- def length
184
- scan_mailbox
185
- @ids.length
186
- end
187
- alias size length
110
+ # Gets a Larch::IMAP::Mailbox instance representing the specified mailbox. If
111
+ # the mailbox doesn't exist and the +:create_mailbox+ option is +false+, or if
112
+ # +:create_mailbox+ is +true+ and mailbox creation fails, a
113
+ # Larch::IMAP::MailboxNotFoundError will be raised.
114
+ def mailbox(name)
115
+ retries = 0
188
116
 
189
- # Gets the IMAP mailbox.
190
- def mailbox
191
- mb = @uri.path[1..-1]
192
- mb.nil? || mb.empty? ? 'INBOX' : CGI.unescape(mb)
117
+ begin
118
+ @mailboxes.fetch(name) do
119
+ update_mailboxes
120
+ return @mailboxes[name] if @mailboxes.has_key?(name)
121
+ raise MailboxNotFoundError, "mailbox not found: #{name}"
122
+ end
123
+
124
+ rescue MailboxNotFoundError => e
125
+ raise unless @options[:create_mailbox] && retries == 0
126
+
127
+ info "creating mailbox: #{name}"
128
+ safely { @conn.create(name) }
129
+
130
+ retries += 1
131
+ retry
132
+ end
193
133
  end
194
134
 
135
+ # Sends an IMAP NOOP command.
195
136
  def noop
196
- safely { @imap.noop }
137
+ safely { @conn.noop }
197
138
  end
198
139
 
199
- # Same as fetch, but doesn't mark the message as seen.
200
- def peek(message_id)
201
- fetch(message_id, true)
140
+ # Gets the IMAP password.
141
+ def password
142
+ CGI.unescape(@uri.password)
202
143
  end
203
144
 
204
145
  # Gets the IMAP port number.
@@ -206,50 +147,52 @@ class IMAP
206
147
  @uri.port || (ssl? ? 993 : 143)
207
148
  end
208
149
 
209
- # Fetches message headers from the current mailbox.
210
- def scan_mailbox
211
- return if @last_scan && (Time.now - @last_scan) < SCAN_INTERVAL
150
+ # Connect if necessary, execute the given block, retry up to 3 times if a
151
+ # recoverable error occurs, die if an unrecoverable error occurs.
152
+ def safely
153
+ safe_connect
154
+
155
+ retries = 0
212
156
 
213
157
  begin
214
- imap_examine
215
- rescue Error => e
216
- return if @options[:create_mailbox]
217
- raise
218
- end
158
+ yield
219
159
 
220
- last_id = safely { @imap.responses['EXISTS'].last }
160
+ rescue Errno::ECONNRESET,
161
+ Errno::ENOTCONN,
162
+ Errno::EPIPE,
163
+ Errno::ETIMEDOUT,
164
+ Net::IMAP::ByeResponseError,
165
+ OpenSSL::SSL::SSLError => e
221
166
 
222
- @mutex.synchronize { @last_scan = Time.now }
223
- return if last_id == @last_id
167
+ raise unless (retries += 1) <= @options[:max_retries]
224
168
 
225
- range = (@last_id + 1)..last_id
169
+ info "#{e.class.name}: #{e.message} (reconnecting)"
226
170
 
227
- @mutex.synchronize { @last_id = last_id }
171
+ reset
172
+ sleep 1 * retries
173
+ safe_connect
174
+ retry
228
175
 
229
- info "fetching message headers #{range}" <<
230
- (@options[:fast_scan] ? ' (fast scan)' : '')
176
+ rescue Net::IMAP::BadResponseError,
177
+ Net::IMAP::NoResponseError,
178
+ Net::IMAP::ResponseParseError => e
231
179
 
232
- fields = if @options[:fast_scan]
233
- ['UID', 'RFC822.SIZE', 'INTERNALDATE']
234
- else
235
- "(UID BODY.PEEK[HEADER.FIELDS (MESSAGE-ID)] RFC822.SIZE INTERNALDATE)"
236
- end
180
+ raise unless (retries += 1) <= @options[:max_retries]
237
181
 
238
- imap_fetch(range, fields).each do |data|
239
- id = create_id(data)
182
+ info "#{e.class.name}: #{e.message} (will retry)"
240
183
 
241
- unless uid = data.attr['UID']
242
- error "UID not in IMAP response for message: #{id}"
243
- next
244
- end
184
+ sleep 1 * retries
185
+ retry
186
+ end
245
187
 
246
- if Larch.log.level == :debug && @ids.has_key?(id)
247
- envelope = imap_uid_fetch([uid], 'ENVELOPE').first.attr['ENVELOPE']
248
- debug "duplicate message? #{id} (Subject: #{envelope.subject})"
249
- end
188
+ rescue Larch::Error => e
189
+ raise
250
190
 
251
- @mutex.synchronize { @ids[id] = uid }
252
- end
191
+ rescue Net::IMAP::Error => e
192
+ raise Error, "#{e.class.name}: #{e.message} (giving up)"
193
+
194
+ rescue => e
195
+ raise FatalError, "#{e.class.name}: #{e.message} (cannot recover)"
253
196
  end
254
197
 
255
198
  # Gets the SSL status.
@@ -262,126 +205,29 @@ class IMAP
262
205
  @uri.to_s
263
206
  end
264
207
 
265
- private
266
-
267
- # Creates an id suitable for uniquely identifying a specific message across
268
- # servers (we hope).
269
- #
270
- # If the given message data includes a valid Message-Id header, then that will
271
- # be used to generate an MD5 hash. Otherwise, the hash will be generated based
272
- # on the message's RFC822.SIZE and INTERNALDATE.
273
- def create_id(data)
274
- ['RFC822.SIZE', 'INTERNALDATE'].each do |a|
275
- raise FatalError, "requested data not in IMAP response: #{a}" unless data.attr[a]
276
- end
277
-
278
- if data.attr['BODY[HEADER.FIELDS (MESSAGE-ID)]'] =~ REGEX_MESSAGE_ID
279
- Digest::MD5.hexdigest($1)
280
- else
281
- Digest::MD5.hexdigest(sprintf('%d%d', data.attr['RFC822.SIZE'],
282
- Time.parse(data.attr['INTERNALDATE']).to_i))
283
- end
284
- end
285
-
286
- # Examines the mailbox. If _force_ is true, the mailbox will be examined even
287
- # if it is already selected (which isn't necessary unless you want to ensure
288
- # that it's in a read-only state).
289
- def imap_examine(force = false)
290
- return if @mailbox_state == :examined || (!force && @mailbox_state == :selected)
291
-
292
- safely do
293
- begin
294
- @mutex.synchronize { @mailbox_state = :closed }
295
-
296
- debug "examining mailbox: #{mailbox}"
297
- @imap.examine(mailbox)
298
-
299
- @mutex.synchronize { @mailbox_state = :examined }
300
-
301
- rescue Net::IMAP::NoResponseError => e
302
- raise Error, "unable to examine mailbox: #{e.message}"
303
- end
304
- end
305
- end
306
-
307
- # Fetches the specified _fields_ for the specified message sequence id(s) from
308
- # the IMAP server.
309
- def imap_fetch(ids, fields)
310
- ids = ids.to_a
311
- data = []
312
- pos = 0
313
-
314
- while pos < ids.length
315
- safely do
316
- imap_examine
317
-
318
- data += @imap.fetch(ids[pos, MAX_FETCH_COUNT], fields)
319
- pos += MAX_FETCH_COUNT
320
- end
321
- end
322
-
323
- data
208
+ # Gets the IMAP mailbox specified in the URI, or +nil+ if none.
209
+ def uri_mailbox
210
+ mb = @uri.path[1..-1]
211
+ mb.nil? || mb.empty? ? nil : CGI.unescape(mb)
324
212
  end
325
213
 
326
- # Selects the mailbox if it is not already selected. If the mailbox does not
327
- # exist and _create_ is +true+, it will be created. Otherwise, a
328
- # Larch::IMAP::Error will be raised.
329
- def imap_select(create = false)
330
- return if @mailbox_state == :selected
331
-
332
- safely do
333
- begin
334
- @mutex.synchronize { @mailbox_state = :closed }
335
-
336
- debug "selecting mailbox: #{mailbox}"
337
- @imap.select(mailbox)
338
-
339
- @mutex.synchronize { @mailbox_state = :selected }
340
-
341
- rescue Net::IMAP::NoResponseError => e
342
- raise Error, "unable to select mailbox: #{e.message}" unless create
343
-
344
- info "creating mailbox: #{mailbox}"
345
-
346
- begin
347
- @imap.create(mailbox)
348
- retry
349
- rescue => e
350
- raise Error, "unable to create mailbox: #{e.message}"
351
- end
352
- end
353
- end
214
+ # Gets the IMAP username.
215
+ def username
216
+ CGI.unescape(@uri.user)
354
217
  end
355
218
 
356
- # Fetches the specified _fields_ for the specified UID(s) from the IMAP
357
- # server.
358
- def imap_uid_fetch(uids, fields)
359
- uids = uids.to_a
360
- data = []
361
- pos = 0
362
-
363
- while pos < uids.length
364
- safely do
365
- imap_examine
366
-
367
- data += @imap.uid_fetch(uids[pos, MAX_FETCH_COUNT], fields)
368
- pos += MAX_FETCH_COUNT
369
- end
370
- end
371
-
372
- data
373
- end
219
+ private
374
220
 
375
221
  # Resets the connection and mailbox state.
376
222
  def reset
377
223
  @mutex.synchronize do
378
- @imap = nil
379
- @mailbox_state = :closed
224
+ @conn = nil
225
+ @mailboxes.each_value {|mb| mb.reset }
380
226
  end
381
227
  end
382
228
 
383
229
  def safe_connect
384
- return if @imap
230
+ return if @conn
385
231
 
386
232
  retries = 0
387
233
 
@@ -410,54 +256,6 @@ class IMAP
410
256
  raise FatalError, "#{e.class.name}: #{e.message} (cannot recover)"
411
257
  end
412
258
 
413
- # Connect if necessary, execute the given block, retry up to 3 times if a
414
- # recoverable error occurs, die if an unrecoverable error occurs.
415
- def safely
416
- safe_connect
417
-
418
- retries = 0
419
-
420
- begin
421
- yield
422
-
423
- rescue Errno::ECONNRESET,
424
- Errno::ENOTCONN,
425
- Errno::EPIPE,
426
- Errno::ETIMEDOUT,
427
- Net::IMAP::ByeResponseError,
428
- OpenSSL::SSL::SSLError => e
429
-
430
- raise unless (retries += 1) <= @options[:max_retries]
431
-
432
- info "#{e.class.name}: #{e.message} (reconnecting)"
433
-
434
- reset
435
- sleep 1 * retries
436
- safe_connect
437
- retry
438
-
439
- rescue Net::IMAP::BadResponseError,
440
- Net::IMAP::NoResponseError,
441
- Net::IMAP::ResponseParseError => e
442
-
443
- raise unless (retries += 1) <= @options[:max_retries]
444
-
445
- info "#{e.class.name}: #{e.message} (will retry)"
446
-
447
- sleep 1 * retries
448
- retry
449
- end
450
-
451
- rescue Larch::Error => e
452
- raise
453
-
454
- rescue Net::IMAP::Error => e
455
- raise Error, "#{e.class.name}: #{e.message} (giving up)"
456
-
457
- rescue => e
458
- raise FatalError, "#{e.class.name}: #{e.message} (cannot recover)"
459
- end
460
-
461
259
  def unsafe_connect
462
260
  info "connecting..."
463
261
 
@@ -465,7 +263,7 @@ class IMAP
465
263
 
466
264
  Thread.new do
467
265
  begin
468
- @imap = Net::IMAP.new(host, port, ssl?,
266
+ @conn = Net::IMAP.new(host, port, ssl?,
469
267
  ssl? && @options[:ssl_verify] ? @options[:ssl_certs] : nil,
470
268
  @options[:ssl_verify])
471
269
 
@@ -473,7 +271,7 @@ class IMAP
473
271
 
474
272
  auth_methods = ['PLAIN']
475
273
  tried = []
476
- capability = @imap.capability
274
+ capability = @conn.capability
477
275
 
478
276
  ['LOGIN', 'CRAM-MD5'].each do |method|
479
277
  auth_methods << method if capability.include?("AUTH=#{method}")
@@ -485,9 +283,9 @@ class IMAP
485
283
  debug "authenticating using #{method}"
486
284
 
487
285
  if method == 'PLAIN'
488
- @imap.login(@username, @password)
286
+ @conn.login(username, password)
489
287
  else
490
- @imap.authenticate(method, @username, @password)
288
+ @conn.authenticate(method, username, password)
491
289
  end
492
290
 
493
291
  info "authenticated using #{method}"
@@ -506,6 +304,21 @@ class IMAP
506
304
 
507
305
  raise exception if exception
508
306
  end
307
+
308
+ def update_mailboxes
309
+ list = safely { @conn.list('', '*') }
310
+
311
+ @mutex.synchronize do
312
+ # Remove cached mailboxes that no longer exist.
313
+ @mailboxes.delete_if {|k, v| !list.any?{|mb| mb.name == k}}
314
+
315
+ # Update cached mailboxes.
316
+ list.each do |mb|
317
+ @mailboxes[mb.name] ||= Mailbox.new(self, mb.name, mb.delim, mb.attr)
318
+ end
319
+ end
320
+ end
321
+
509
322
  end
510
323
 
511
324
  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.7
4
+ version: 1.0.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Grove
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-03-26 00:00:00 -07:00
12
+ date: 2009-04-08 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency