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

Archive, IDLE, Keywords

Defined Under Namespace

Classes: Archive, Connection, Error, IDLE, Keywords

Constant Summary collapse

VERSION =

The version of IMAPProcessor you are using

'1.2'
@@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



305
306
307
308
309
310
# File 'lib/imap_processor.rb', line 305

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



55
56
57
# File 'lib/imap_processor.rb', line 55

def imap
  @imap
end

#optionsObject (readonly)

Options Hash from process_args



60
61
62
# File 'lib/imap_processor.rb', line 60

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.



70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/imap_processor.rb', line 70

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

NOTE: You can add a –move option using ::add_move



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
272
273
274
275
276
277
278
279
280
281
# File 'lib/imap_processor.rb', line 109

def self.process_args(processor_file, args,
                      required_options = {}) # :yield: OptionParser
  opts_file_name = File.basename processor_file, '.rb'
  opts_file_name = "imap_#{opts_file_name}" unless opts_file_name =~ /^imap_/
  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

  op = 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

  op.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.



286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/imap_processor.rb', line 286

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 = ) ⇒ 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.



319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
# File 'lib/imap_processor.rb', line 319

def connect(host = @options[:Host],
            port = @options[:Port],
            ssl = @options[:SSL],
            username = @options[:Username],
            password = @options[:Password],
            auth = @options[:Auth]) # :yields: Connection
  imap = Net::IMAP.new host, port, ssl, nil, false
  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 = Connection.new imap, capability

  if block_given? then
    begin
      yield connection
    ensure
      connection.imap.logout
    end
  else
    return connection
  end
end

#create_mailbox(name) ⇒ Object

Create the mailbox name if it doesn’t exist. Note that this will SELECT the mailbox if it exists.



361
362
363
364
365
366
367
# File 'lib/imap_processor.rb', line 361

def create_mailbox name
  log "LIST #{name}"
  list = imap.list '', name
  return if list
  log "CREATE #{name}"
  imap.create name
end

#delete_messages(uids, expunge = true) ⇒ Object

Delete and expunge the specified uids.



372
373
374
375
376
377
378
379
# File 'lib/imap_processor.rb', line 372

def delete_messages uids, expunge = true
  log "DELETING [...#{uids.size} uids]"
  imap.store uids, '+FLAGS.SILENT', [:Deleted]
  if expunge then
    log "EXPUNGE"
    imap.expunge
  end
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.

If the block returns nil or false, the message is considered skipped and its uid is not returned in the uid list. (Hint: next false unless …)

Returns the uids of successfully handled messages.



393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
# File 'lib/imap_processor.rb', line 393

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

  uids = []

  each_part parts, true do |uid, message|
    skip = false

    mail = TMail::Mail.parse message

    begin
      success = yield uid, mail

      uids << uid if success
    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.



425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
# File 'lib/imap_processor.rb', line 425

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



446
447
448
449
# File 'lib/imap_processor.rb', line 446

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.



460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
# File 'lib/imap_processor.rb', line 460

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

#move_messages(uids, destination, expunge = true) ⇒ Object

Move the specified uids to a new destination then delete and expunge them. Creates the destination mailbox if it doesn’t exist.



492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
# File 'lib/imap_processor.rb', line 492

def move_messages uids, destination, expunge = true
  return if uids.empty?
  log "COPY [...#{uids.size} uids]"

  begin
    imap.copy uids, destination
  rescue Net::IMAP::NoResponseError => e
    # ruby-lang bug #1713
    #raise unless e.response.data.code.name == 'TRYCREATE'
    create_mailbox destination
    imap.copy uids, destination
  end

  delete_messages uids, expunge
end

#show_messages(uids) ⇒ Object

Displays Date, Subject and Message-Id from messages in uids



511
512
513
514
515
516
517
518
519
520
521
# File 'lib/imap_processor.rb', line 511

def show_messages(uids)
  return if uids.nil? or (Array === uids and uids.empty?)

  fetch_data = 'BODY.PEEK[HEADER.FIELDS (DATE SUBJECT MESSAGE-ID)]'
  messages = imap.fetch uids, fetch_data
  fetch_data.sub! '.PEEK', '' # stripped by server

  messages.each do |res|
    puts res.attr[fetch_data].delete("\r")
  end
end

#verbose?Boolean

Did the user set –verbose?

Returns:

  • (Boolean)


526
527
528
# File 'lib/imap_processor.rb', line 526

def verbose?
  @verbose
end