class IMAPProcessor

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

Subclasses need to provide:

Reference:

email: http://www.ietf.org/rfc/rfc0822.txt
 imap: http://www.ietf.org/rfc/rfc3501.txt

Constants

VERSION

The version of IMAPProcessor you are using

Attributes

imap[R]

Net::IMAP connection, set this via initialize

options[R]

Options Hash from process_args

Public Class Methods

add_move() click to toggle source

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.

# File lib/imap_processor.rb, line 78
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
new(options) click to toggle source

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

# File lib/imap_processor.rb, line 339
def initialize(options)
  @options = options
  @verbose = options[:Verbose]
  @boxes = options[:Boxes]
  Net::IMAP.debug = options[:Debug]
end
process_args(processor_file, args, required_options = {}) { |OptionParser| ... } click to toggle source

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
end

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

# File lib/imap_processor.rb, line 118
  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}"

    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

    defaults = [{}]

    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

      defaults = Array(YAML.load_file(opts_file))
    end

    defaults.map { |default|
      options = default.merge @@options.dup

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

      required_options.each do |k,(v,_)|
        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_tail("-h", "--help", "Show this message") do
          puts opts
          exit
        end

        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.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("-n", "--noop",
                "Perform no destructive operations",
                "Best used with the verbose option",
                "Default: #{options[:Noop].inspect}",
                "Options file name: Noop") do |noop|
          options[:Noop] = noop
        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 # OptionParser.new do

      op.parse! args.dup

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

      # HACK: removed :Boxes -- push down
      required_keys = [:Host, :Password] + required_options.keys
      if required_keys.any? { |k| options[k].nil? } then
        $stderr.puts op
        $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, (_, missing_message)|
          $stderr.puts missing_message if options[option_name].nil?
        end
        exit 1
      end

      options
    } # defaults.map
  end
run(args = ARGV, &block) click to toggle source

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

# File lib/imap_processor.rb, line 314
def self.run(args = ARGV, &block)
  client = nil
  multi_options = process_args args

  multi_options.each do |options|
    client = new(options, &block)
    client.run
  end
rescue Interrupt
  exit
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 and client.imap
end

Public Instance Methods

capability(imap, res = nil) click to toggle source

Extracts capability information for imap from res or by contacting the server.

# File lib/imap_processor.rb, line 350
def capability imap, res = nil
  return imap.capability unless res

  data = res.data

  if data.code and data.code.name == 'CAPABILITY' then
    case data.code.data
    when Array then
      data.code.data
    when String then
      data.code.data.split ' '
    else
      raise ArgumentError, "unknown type: #{data.code.data.class}"
    end
  else
    imap.capability
  end
end
connect(host = @options[:Host], port = @options[:Port], ssl = @options[:SSL], username = @options[:Username], password = @options[:Password], auth = @options[:Auth]) { |Connection| ... } click to toggle source

Connects to IMAP server host at port using ssl if ssl is true then authenticates with username and password. IMAPProcessor is only known to work with PLAIN auth on SSL sockets. IMAPProcessor does not support LOGIN.

Returns a Connection object.

# File lib/imap_processor.rb, line 377
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}/"

  capabilities = capability imap, imap.greeting

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

  auth_caps = capabilities.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

  # Net::IMAP supports using AUTHENTICATE with LOGIN, PLAIN, and
  # CRAM-MD5... if the server reports a different AUTH method, then we
  # should fall back to using LOGIN
  if %w( LOGIN PLAIN CRAM-MD5 XOAUTH2 ).include?( auth.upcase )
    auth = auth.upcase
    log "Trying #{auth} authentication"
    res = imap.authenticate auth, username, password
    log "Logged in as #{username} using AUTHENTICATE"
  else
    log "Trying to authenticate via LOGIN"
    res = imap.login username, password
    log "Logged in as #{username} using LOGIN"
  end

  # CAPABILITY may have changed
  capabilities = capability imap, res

  connection = Connection.new imap, capabilities

  if block_given? then
    begin
      yield connection
    ensure
      connection.imap.logout
    end
  else
    return connection
  end
end
create_mailbox(name) click to toggle source

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

# File lib/imap_processor.rb, line 431
def create_mailbox name
  log "LIST #{name}"
  list = imap.list '', name
  return if list
  log "CREATE #{name}"
  imap.create name unless noop?
end
delete_messages(uids, expunge = true) click to toggle source

Delete and expunge the specified uids.

# File lib/imap_processor.rb, line 442
def delete_messages uids, expunge = true
  log "DELETING [...#{uids.size} uids]"
  imap.store uids, '+FLAGS.SILENT', [:Deleted] unless noop?
  if expunge then
    log "EXPUNGE"
    imap.expunge unless noop?
  end
end
each_message(uids, type) { |Mail| ... } click to toggle source

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.

# File lib/imap_processor.rb, line 463
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
      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) { |uid, message| ... } click to toggle source

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

# File lib/imap_processor.rb, line 493
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) click to toggle source

Logs message to $stderr if verbose

# File lib/imap_processor.rb, line 514
def log(message)
  return unless @verbose
  $stderr.puts "# #{message}"
end
mime_parts(uids, mime_type) click to toggle source

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.

# File lib/imap_processor.rb, line 528
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) click to toggle source

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

# File lib/imap_processor.rb, line 560
def move_messages uids, destination, expunge = true
  return if uids.empty?
  verb = expunge ? "MOVE" : "COPY"
  log "%s %d uids to %s:" % [verb, uids.size, destination]

  begin
    imap.copy uids, destination unless noop?
  rescue Net::IMAP::NoResponseError
    unless noop? then
      create_mailbox destination
      imap.copy uids, destination
    end
  end

  delete_messages uids, expunge
end
noop?() click to toggle source

Did the user set –noop?

# File lib/imap_processor.rb, line 604
def noop?
  options[:Noop]
end
show_messages(uids) click to toggle source

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

# File lib/imap_processor.rb, line 580
def show_messages(uids)
  return if uids.nil? or (Array === uids and uids.empty?)

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

  messages ||= []

  messages.each do |res|
    puts res.attr[fetch_data].delete("\r").gsub(/^/, "  ")
  end
end
verbose?() click to toggle source

Did the user set –verbose?

# File lib/imap_processor.rb, line 597
def verbose?
  @verbose
end