rgrove-larch 1.0.0.7 → 1.0.0.8

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