Class: Larch::IMAP

Inherits:
Object
  • Object
show all
Defined in:
lib/larch/imap.rb,
lib/larch/errors.rb,
lib/larch/imap/mailbox.rb

Overview

Manages a connection to an IMAP server and all the glorious fun that entails.

This class borrows heavily from Sup, the source code of which should be required reading if you’re doing anything with IMAP in Ruby: sup.rubyforge.org

Defined Under Namespace

Classes: Error, FatalError, Mailbox, MailboxNotFoundError, Message, MessageNotFoundError

Constant Summary collapse

REGEX_URI =

URI format validation regex.

URI.regexp(['imap', 'imaps'])

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(uri, options = {}) ⇒ IMAP

Initializes a new Larch::IMAP instance that will connect to the specified IMAP URI.

In addition to the URI, the following options may be specified:

:create_mailbox

If true, mailboxes that don’t already exist will be created if necessary.

:dry_run

If true, read-only operations will be performed as usual and all change operations will be simulated, but no changes will actually be made. Note that it’s not actually possible to simulate mailbox creation, so :dry_run mode always behaves as if :create_mailbox is false.

:log_label

Label to use for this connection in log output. If not specified, the default label is “[username@host]”.

:max_retries

After a recoverable error occurs, retry the operation up to this many times. Default is 3.

:ssl_certs

Path to a trusted certificate bundle to use to verify server SSL certificates. You can download a bundle of certificate authority root certs at curl.haxx.se/ca/cacert.pem (it’s up to you to verify that this bundle hasn’t been tampered with, however; don’t trust it blindly).

:ssl_verify

If true, server SSL certificates will be verified against the trusted certificate bundle specified in ssl_certs. By default, server SSL certificates are not verified.

Raises:

  • (ArgumentError)


52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/larch/imap.rb', line 52

def initialize(uri, options = {})
  raise ArgumentError, "not an IMAP URI: #{uri}" unless uri.is_a?(URI) || uri =~ REGEX_URI
  raise ArgumentError, "options must be a Hash" unless options.is_a?(Hash)

  @uri     = uri.is_a?(URI) ? uri : URI(uri)
  @options = {
    :log_label   => "[#{username}@#{host}]",
    :max_retries => 3,
    :ssl_verify  => false
  }.merge(options)

  raise ArgumentError, "must provide a username and password" unless @uri.user && @uri.password

  @conn      = nil
  @mailboxes = {}

  @quirks    = {
    :gmail => false,
    :yahoo => false
  }

  @db_account = Database::Account.find_or_create(
    :hostname => host,
    :username => username
  )

  @db_account.touch

  # Create private convenience methods (debug, info, warn, etc.) to make
  # logging easier.
  Logger::LEVELS.each_key do |level|
    next if IMAP.private_method_defined?(level)

    IMAP.class_eval do
      define_method(level) do |msg|
        Larch.log.log(level, "#{@options[:log_label]} #{msg}")
      end

      private level
    end
  end
end

Instance Attribute Details

#connObject (readonly)

Returns the value of attribute conn.



9
10
11
# File 'lib/larch/imap.rb', line 9

def conn
  @conn
end

#db_accountObject (readonly)

Returns the value of attribute db_account.



9
10
11
# File 'lib/larch/imap.rb', line 9

def 
  @db_account
end

#mailboxesObject (readonly)

Returns the value of attribute mailboxes.



9
10
11
# File 'lib/larch/imap.rb', line 9

def mailboxes
  @mailboxes
end

#optionsObject (readonly)

Returns the value of attribute options.



9
10
11
# File 'lib/larch/imap.rb', line 9

def options
  @options
end

#quirksObject (readonly)

Returns the value of attribute quirks.



9
10
11
# File 'lib/larch/imap.rb', line 9

def quirks
  @quirks
end

Instance Method Details

#connectObject

Connects to the IMAP server and logs in if a connection hasn’t already been established.



97
98
99
100
# File 'lib/larch/imap.rb', line 97

def connect
  return if @conn
  safely {} # connect, but do nothing else
end

#delimObject

Gets the server’s mailbox hierarchy delimiter.



103
104
105
# File 'lib/larch/imap.rb', line 103

def delim
  @delim ||= safely { @conn.list('', '')[0].delim || '.'}
end

#disconnectObject

Closes the IMAP connection if one is currently open.



108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/larch/imap.rb', line 108

def disconnect
  return unless @conn

  begin
    @conn.disconnect
  rescue Errno::ENOTCONN => e
    debug "#{e.class.name}: #{e.message}"
  end

  reset

  info "disconnected"
end

#each_mailboxObject

Iterates through all mailboxes in the account, yielding each one as a Larch::IMAP::Mailbox instance to the given block.



124
125
126
127
# File 'lib/larch/imap.rb', line 124

def each_mailbox
  update_mailboxes
  @mailboxes.each_value {|mailbox| yield mailbox }
end

#hostObject

Gets the IMAP hostname.



130
131
132
# File 'lib/larch/imap.rb', line 130

def host
  @uri.host
end

#mailbox(name, delim = '/') ⇒ Object

Gets a Larch::IMAP::Mailbox instance representing the specified mailbox. If the mailbox doesn’t exist and the :create_mailbox option is false, or if :create_mailbox is true and mailbox creation fails, a Larch::IMAP::MailboxNotFoundError will be raised.



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/larch/imap.rb', line 138

def mailbox(name, delim = '/')
  retries = 0

  name.gsub!(/^(inbox\/?)/i){ $1.upcase }
  name.gsub!(delim, self.delim)

  # Gmail doesn't allow folders with leading or trailing whitespace.
  name.strip! if @quirks[:gmail]
  
  #Rackspace namespaces everything under INDEX.
  name.sub!(/^|inbox\./i, "INBOX.") if @quirks[:rackspace] && name != 'INBOX'

  begin
    @mailboxes.fetch(name) do
      update_mailboxes
      return @mailboxes[name] if @mailboxes.has_key?(name)
      raise MailboxNotFoundError, "mailbox not found: #{name}"
    end

  rescue MailboxNotFoundError => e
    raise unless @options[:create_mailbox] && retries == 0

    info "creating mailbox: #{name}"
    safely { @conn.create(Net::IMAP.encode_utf7(name)) } unless @options[:dry_run]

    retries += 1
    retry
  end
end

#noopObject

Sends an IMAP NOOP command.



169
170
171
# File 'lib/larch/imap.rb', line 169

def noop
  safely { @conn.noop }
end

#passwordObject

Gets the IMAP password.



174
175
176
# File 'lib/larch/imap.rb', line 174

def password
  CGI.unescape(@uri.password)
end

#portObject

Gets the IMAP port number.



179
180
181
# File 'lib/larch/imap.rb', line 179

def port
  @uri.port || (ssl? ? 993 : 143)
end

#safelyObject

Connect if necessary, execute the given block, retry if a recoverable error occurs, die if an unrecoverable error occurs.



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/larch/imap.rb', line 185

def safely
  safe_connect

  retries = 0

  begin
    yield

  rescue Errno::ECONNABORTED,
         Errno::ECONNRESET,
         Errno::ENOTCONN,
         Errno::EPIPE,
         Errno::ETIMEDOUT,
         IOError,
         Net::IMAP::ByeResponseError,
         OpenSSL::SSL::SSLError => e

    raise unless (retries += 1) <= @options[:max_retries]

    warning "#{e.class.name}: #{e.message} (reconnecting)"

    reset
    sleep 1 * retries
    safe_connect
    retry

  rescue Net::IMAP::BadResponseError,
         Net::IMAP::NoResponseError,
         Net::IMAP::ResponseParseError => e

    raise unless (retries += 1) <= @options[:max_retries]

    warning "#{e.class.name}: #{e.message} (will retry)"

    sleep 1 * retries
    retry
  end

rescue Larch::Error => e
  raise

rescue Net::IMAP::Error => e
  raise Error, "#{e.class.name}: #{e.message} (giving up)"

rescue => e
  raise FatalError, "#{e.class.name}: #{e.message} (cannot recover)"
end

#ssl?Boolean

Gets the SSL status.

Returns:

  • (Boolean)


234
235
236
# File 'lib/larch/imap.rb', line 234

def ssl?
  @uri.scheme == 'imaps'
end

#uriObject

Gets the IMAP URI.



239
240
241
# File 'lib/larch/imap.rb', line 239

def uri
  @uri.to_s
end

#uri_mailboxObject

Gets the IMAP mailbox specified in the URI, or nil if none.



244
245
246
247
# File 'lib/larch/imap.rb', line 244

def uri_mailbox
  mb = @uri.path[1..-1]
  mb.nil? || mb.empty? ? nil : CGI.unescape(mb)
end

#usernameObject

Gets the IMAP username.



250
251
252
# File 'lib/larch/imap.rb', line 250

def username
  CGI.unescape(@uri.user)
end