Parent

IMAPProcessor

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

Subclasses need to provide:

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 71
  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 309
  def initialize(options)
    @options = options
    @verbose = options[:Verbose]
    @boxes = options[:Boxes]
    Net::IMAP.debug = options[:Debug]
  end
process_args(processor_file, args, required_options = {}) 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

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

# File lib/imap_processor.rb, line 110
  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 unless options.key? :SSL
    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 << "\nOptions may also be set in the options file ~/.\#{opts_file_name}\n\nExample ~/.\#{opts_file_name}:\n\\tHost=mail.example.com\n\\tPassword=my password\n\n"
    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 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, (option_value, missing_message)|
        $stderr.puts missing_message if options[option_name].nil?
      end
      exit 1
    end

    return options
  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 288
  def self.run(args = ARGV, &block)
    options = process_args args
    client = new(options, &block)
    client.run
  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

connect(host = @options[:Host], port = @options[:Port], ssl = @options[:SSL], username = @options[:Username], password = @options[:Password], auth = @options[:Auth]) click to toggle source

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.

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

Delete and expunge the specified uids.

# File lib/imap_processor.rb, line 376
  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) 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 397
  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) 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 429
  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 450
  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 464
  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 496
  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) click to toggle source

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

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

Did the user set —verbose?

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

Disabled; run with --debug to generate this.

[Validate]

Generated with the Darkfish Rdoc Generator 1.1.6.