Class: IMAPProcessor

Inherits:
Object
  • Object
show all
Defined in:
lib/imap_processor.rb

Overview

IMAPProcessor is a client for processing messages on an IMAP server.

Subclasses need to provide:

  • A process_args class method that adds any extra options to the default IMAPProcessor options.

  • An initialize method that connects to an IMAP server and sets the @imap instance variable

  • A run method that uses the IMAP connection to process messages.

Direct Known Subclasses

Keywords

Defined Under Namespace

Classes: Connection, Keywords

Constant Summary collapse

VERSION =

The version of IMAPProcessor you are using

'1.0.1'
@@options =
{}
@@extra_options =
[]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options) ⇒ IMAPProcessor

Handles the basic settings from options including verbosity, mailboxes to process, and Net::IMAP::debug



295
296
297
298
299
300
# File 'lib/imap_processor.rb', line 295

def initialize(options)
  @options = options
  @verbose = options[:Verbose]
  @boxes = options[:Boxes]
  Net::IMAP.debug = options[:Debug]
end

Instance Attribute Details

#imapObject (readonly)

Net::IMAP connection, set this via #initialize



48
49
50
# File 'lib/imap_processor.rb', line 48

def imap
  @imap
end

#optionsObject (readonly)

Options Hash from process_args



53
54
55
# File 'lib/imap_processor.rb', line 53

def options
  @options
end

Class Method Details

.add_moveObject

Adds a –move option to the option parser which stores the destination mailbox in the MoveTo option. Call this from a subclass’ process_args method.



63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/imap_processor.rb', line 63

def self.add_move
  @@options[:MoveTo] = nil

  @@extra_options << proc do |opts, options|
    opts.on(      "--move=MAILBOX",
            "Mailbox to move message to",
            "Default: #{options[:MoveTo].inspect}",
            "Options file name: MoveTo") do |mailbox|
      options[:MoveTo] = mailbox
    end
  end
end

.process_args(processor_file, args, required_options = {}) ⇒ Object

Handles processing of args loading defaults from a file in ~ based on processor_file. Extra option defaults can be specified by required_options. Yields an option parser instance to add new OptionParser options to:

class MyProcessor < IMAPProcessor
  def self.process_args(args)
    required_options = {
      :MoveTo => [nil, "MoveTo not set"],
    }

  super __FILE__, args, required_options do |opts, options|
    opts.banner << "Explain my_processor's executable"

    opts.on(      "--move=MAILBOX",
            "Mailbox to move message to",
            "Default: #{options[:MoveTo].inspect}",
            "Options file name: MoveTo") do |mailbox|
      options[:MoveTo] = mailbox
    end
  end
end


100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
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
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'lib/imap_processor.rb', line 100

def self.process_args(processor_file, args,
                      required_options = {}) # :yield: OptionParser
  opts_file_name = File.basename processor_file, '.rb'
  opts_file = File.expand_path "~/.#{opts_file_name}"
  options = @@options.dup

  if required_options then
    required_options.each do |option, (default, message)|
      raise ArgumentError,
            "required_options message is missing for #{option}" if
        default.nil? and message.nil?
    end
  end

  if File.exist? opts_file then
    unless File.stat(opts_file).mode & 077 == 0 then
      $stderr.puts "WARNING! #{opts_file} is group/other readable or writable!"
      $stderr.puts "WARNING! I'm not doing a thing until you fix it!"
      exit 1
    end

    options.merge! YAML.load_file(opts_file)
  end

  options[:SSL]      ||= true
  options[:Username] ||= ENV['USER']
  options[:Root]     ||= nil
  options[:Verbose]  ||= false
  options[:Debug]    ||= false

  required_options.each do |k,(v,m)|
    options[k]       ||= v
  end

  opts = OptionParser.new do |opts|
    opts.program_name = File.basename $0
    opts.banner = "Usage: #{opts.program_name} [options]\n\n"

    opts.separator ''
    opts.separator 'Connection options:'

    opts.on("-H", "--host HOST",
            "IMAP server host",
            "Default: #{options[:Host].inspect}",
            "Options file name: Host") do |host|
      options[:Host] = host
    end

    opts.on("-P", "--port PORT",
            "IMAP server port",
            "Default: The correct port SSL/non-SSL mode",
            "Options file name: Port") do |port|
      options[:Port] = port
    end

    opts.on("-s", "--[no-]ssl",
            "Use SSL for IMAP connection",
            "Default: #{options[:SSL].inspect}",
            "Options file name: SSL") do |ssl|
      options[:SSL] = ssl
    end

    opts.on(      "--[no-]debug",
            "Display Net::IMAP debugging info",
            "Default: #{options[:Debug].inspect}",
            "Options file name: Debug") do |debug|
      options[:Debug] = debug
    end

    opts.separator ''
    opts.separator 'Login options:'

    opts.on("-u", "--username USERNAME",
            "IMAP username",
            "Default: #{options[:Username].inspect}",
            "Options file name: Username") do |username|
      options[:Username] = username
    end

    opts.on("-p", "--password PASSWORD",
            "IMAP password",
            "Default: Read from ~/.#{opts_file_name}",
            "Options file name: Password") do |password|
      options[:Password] = password
    end

    authenticators = Net::IMAP.send :class_variable_get, :@@authenticators
    auth_types = authenticators.keys.sort.join ', '
    opts.on("-a", "--auth AUTH", auth_types,
            "IMAP authentication type override",
            "Authentication type will be auto-",
            "discovered",
            "Default: #{options[:Auth].inspect}",
            "Options file name: Auth") do |auth|
      options[:Auth] = auth
    end

    opts.separator ''
    opts.separator "IMAP options:"

    opts.on("-r", "--root ROOT",
            "Root of mailbox hierarchy",
            "Default: #{options[:Root].inspect}",
            "Options file name: Root") do |root|
      options[:Root] = root
    end

    opts.on("-b", "--boxes BOXES", Array,
            "Comma-separated list of mailbox names",
            "to search",
            "Default: #{options[:Boxes].inspect}",
            "Options file name: Boxes") do |boxes|
      options[:Boxes] = boxes
    end

    opts.on("-v", "--[no-]verbose",
            "Be verbose",
            "Default: #{options[:Verbose].inspect}",
            "Options file name: Verbose") do |verbose|
      options[:Verbose] = verbose
    end

    opts.on("-q", "--quiet",
            "Be quiet") do
      options[:Verbose] = false
    end

    if block_given? then
      opts.separator ''
      opts.separator "#{self} options:"

      yield opts, options if block_given?
    end

    @@extra_options.each do |block|
      block.call opts, options
    end

    opts.separator ''

    opts.banner << <<-EOF

Options may also be set in the options file ~/.#{opts_file_name}

Example ~/.#{opts_file_name}:
\tHost=mail.example.com
\tPassword=my password

    EOF
  end

  opts.parse! args

  options[:Port] ||= options[:SSL] ? 993 : 143

  if options[:Host].nil? or
     options[:Password].nil? or
     options[:Boxes].nil? or
     required_options.any? { |k,(v,m)| options[k].nil? } then
    $stderr.puts opts
    $stderr.puts
    $stderr.puts "Host name not set" if options[:Host].nil?
    $stderr.puts "Password not set"  if options[:Password].nil?
    $stderr.puts "Boxes not set"     if options[:Boxes].nil?
    required_options.each do |option_name, (option_value, missing_message)|
      $stderr.puts missing_message if options[option_name].nil?
    end
    exit 1
  end

  return options
end

.run(args = ARGV, &block) ⇒ Object

Sets up an IMAP processor’s options then calls its #run method.



276
277
278
279
280
281
282
283
284
285
286
287
288
289
# File 'lib/imap_processor.rb', line 276

def self.run(args = ARGV, &block)
  options = process_args args
  client = new(options, &block)
  client.run
rescue SystemExit
  raise
rescue Exception => e
  $stderr.puts "Failed to finish with exception: #{e.class}:#{e.message}"
  $stderr.puts "\t#{e.backtrace.join "\n\t"}"

  exit 1
ensure
  client.imap.logout if client
end

Instance Method Details

#connect(host, port, ssl, username, password, auth = nil) ⇒ Object

Connects to IMAP server host at port using ssl if ssl is true then logs in as username with password. IMAPProcessor is only known to work with PLAIN auth on SSL sockets.

Returns a Connection object.



309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/imap_processor.rb', line 309

def connect(host, port, ssl, username, password, auth = nil)
  imap = Net::IMAP.new host, port, ssl
  log "Connected to imap://#{host}:#{port}/"

  capability = imap.capability

  log "Capabilities: #{capability.join ', '}"

  auth_caps = capability.select { |c| c =~ /^AUTH/ }

  if auth.nil? then
    raise "Couldn't find a supported auth type" if auth_caps.empty?
    auth = auth_caps.first.sub(/AUTH=/, '')
  end

  auth = auth.upcase
  log "Trying #{auth} authentication"
  imap.authenticate auth, username, password
  log "Logged in as #{username}"

  Connection.new imap, capability
end

#each_message(uids, type) ⇒ Object

Yields each uid and message as a TMail::Message for uids of MIME type type.

If there’s an exception raised during handling a message the subject, message-id and inspected body are logged.

Returns the uids of successfully handled messages.



341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
# File 'lib/imap_processor.rb', line 341

def each_message(uids, type) # :yields: TMail::Mail
  parts = mime_parts uids, type

  uids = []

  each_part parts, true do |uid, message|
    mail = TMail::Mail.parse message

    begin
      yield uid, mail

      uids << uid
    rescue => e
      log e.message
      puts "\t#{e.backtrace.join "\n\t"}" unless $DEBUG # backtrace at bottom
      log "Subject: #{mail.subject}"
      log "Message-Id: #{mail.message_id}"
      p mail.body if verbose?

      raise if $DEBUG
    end
  end

  uids
end

#each_part(parts, header = false) ⇒ Object

Yields each message part from parts. If header is true, a complete message is yielded, appropriately joined for use with TMail::Mail.



371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
# File 'lib/imap_processor.rb', line 371

def each_part(parts, header = false) # :yields: uid, message
  parts.each do |uid, section|
    sequence = ["BODY[#{section}]"]
    sequence.unshift "BODY[#{section}.MIME]" unless section == 'TEXT'
    sequence.unshift 'BODY[HEADER]' if header

    body = @imap.fetch(uid, sequence).first

    sequence = sequence.map { |item| body.attr[item] }

    unless section == 'TEXT' and header then
      sequence[0].sub!(/\r\n\z/, '')
    end

    yield uid, sequence.join
  end
end

#log(message) ⇒ Object

Logs message to $stderr if verbose



392
393
394
395
# File 'lib/imap_processor.rb', line 392

def log(message)
  return unless @verbose
  $stderr.puts "# #{message}"
end

#mime_parts(uids, mime_type) ⇒ Object

Retrieves the BODY data item name for the mime_type part from messages uids. Returns an array of uid/part pairs. If no matching part with mime_type is found the uid is omitted.

Returns an Array of uid, section pairs.

Use a subsequent Net::IMAP#fetch to retrieve the selected part.



406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
# File 'lib/imap_processor.rb', line 406

def mime_parts(uids, mime_type)
  media_type, subtype = mime_type.upcase.split('/', 2)

  structures = @imap.fetch uids, 'BODYSTRUCTURE'

  structures.zip(uids).map do |body, uid|
    section = nil
    structure = body.attr['BODYSTRUCTURE']

    case structure
    when Net::IMAP::BodyTypeMultipart then
      parts = structure.parts

      section = parts.each_with_index do |part, index|
        break index if part.media_type == media_type and
                       part.subtype == subtype
      end

      next unless Integer === section
    when Net::IMAP::BodyTypeText, Net::IMAP::BodyTypeBasic then
      section = 'TEXT' if structure.media_type == media_type and
                          structure.subtype == subtype
    end

    [uid, section]
  end.compact
end

#verbose?Boolean

Did the user set –verbose?

Returns:

  • (Boolean)


437
438
439
# File 'lib/imap_processor.rb', line 437

def verbose?
  @verbose
end