class Stockboy::Providers::IMAP

Read data from a file attachment in IMAP email

Job template DSL

provider :imap do
  host "imap.example.com"
  username "arthur@example.com"
  password "424242"
  mailbox "INBOX"
  subject "Daily Report"
  since Date.today
  file_name /report-[0-9]+\.csv/
end

Public Class Methods

new(opts={}, &block) click to toggle source

Initialize a new IMAP reader

Calls superclass method Stockboy::Provider::new
# File lib/stockboy/providers/imap.rb, line 131
def initialize(opts={}, &block)
  super(opts, &block)
  @host         = opts[:host]
  @username     = opts[:username]
  @password     = opts[:password]
  @mailbox      = opts[:mailbox]
  @subject      = opts[:subject]
  @from         = opts[:from]
  @since        = opts[:since]
  @search       = opts[:search]
  @attachment   = opts[:attachment]
  @file_smaller = opts[:file_smaller]
  @file_larger  = opts[:file_larger]
  @pick         = opts[:pick] || :last
  DSL.new(self).instance_eval(&block) if block_given?
  @open_client  = nil
end

Public Instance Methods

clear() click to toggle source

Clear received data and allow for selecting a new item from the server

Calls superclass method Stockboy::Provider#clear
# File lib/stockboy/providers/imap.rb, line 200
def clear
  super
  @message_key = nil
  @data_time = nil
  @data_size = nil
end
client() { |open_client| ... } click to toggle source

Direct access to the configured Net::IMAP connection

@example

provider.client do |imap|
  imap.search("FLAGGED")
end
# File lib/stockboy/providers/imap.rb, line 156
def client
  raise(ArgumentError, "no block given") unless block_given?
  first_connection = @open_client.nil?
  if first_connection
    @open_client = ::Net::IMAP.new(host)
    @open_client.login(username, password)
    @open_client.examine(mailbox)
  end
  yield @open_client
rescue ::Net::IMAP::Error
  errors << "IMAP connection error"
ensure
  if first_connection && @open_client
    @open_client.disconnect
    @open_client = nil
  end
end
default_search_options() click to toggle source
# File lib/stockboy/providers/imap.rb, line 234
def default_search_options
  {subject: subject, from: from, since: since}.reject { |k,v| v.nil? }
end
delete_data() click to toggle source

Purge the email from the mailbox corresponding to the [#message_key]

This can only be called after selecting the message_key to confirm the selected item, or after fetching the data.

# File lib/stockboy/providers/imap.rb, line 179
def delete_data
  picked_message_key? or raise Stockboy::OutOfSequence,
    "must confirm #message_key or calling #data"

  client do |imap|
    logger.info "Deleting message #{inspect_message_key}"
    imap.uid_store(message_key, "+FLAGS", [:Deleted])
    imap.expunge
  end
end
find_messages(options=nil) click to toggle source

Search the selected mailbox for matching messages

By default, the configured options are used, @param [Hash, Array, String] options

Override default configured search options

@example

provider.find_messages(subject: "Daily Report", before: Date.today)
provider.find_messages(["SUBJECT", "Daily Report", "BEFORE", "21-DEC-12"])
provider.find_messages("FLAGGED BEFORE 21-DEC-12")
# File lib/stockboy/providers/imap.rb, line 218
def find_messages(options=nil)
  client { |imap| imap.sort(['DATE'], search_keys(options), 'UTF-8') }
end
message_key() click to toggle source

IMAP message id for the email that contains the selected data to process

# File lib/stockboy/providers/imap.rb, line 192
def message_key
  return @message_key if @message_key
  message_ids = find_messages(default_search_options)
  @message_key = pick_from(message_ids) unless message_ids.empty?
end
search_keys(options=nil) click to toggle source

Normalize a hash of search options into an array of IMAP search keys

@param [Hash] options If none are given, the configured options are used @return [Array]

# File lib/stockboy/providers/imap.rb, line 227
def search_keys(options=nil)
  case options
  when Array, String then options
  else SearchOptions.new(options || default_search_options).to_imap
  end
end

Private Instance Methods

fetch_data() click to toggle source
# File lib/stockboy/providers/imap.rb, line 240
def fetch_data
  client do |imap|
    open_message(message_key) do |mail|
      open_attachment(mail) do |part|
        logger.debug "Getting file from #{inspect_message_key}"
        @data = part
        @data_time = normalize_imap_datetime(mail.date)
        logger.debug "Got file from #{inspect_message_key}"
      end
    end
  end
  !@data.nil?
end
inspect_message_key() click to toggle source
# File lib/stockboy/providers/imap.rb, line 325
def inspect_message_key
  "#{username}:#{host} message_uid #{message_key}"
end
normalize_imap_datetime(datetime) click to toggle source

If activesupport is loaded, it mucks with DateTime#to_time to return self when it has a utc_offset. Handle both to always return a Time.utc.

# File lib/stockboy/providers/imap.rb, line 296
def normalize_imap_datetime(datetime)
  datetime.respond_to?(:getutc) ?
    datetime.getutc.to_time : datetime.to_time.utc
end
open_attachment(mail) { |decoded| ... } click to toggle source
# File lib/stockboy/providers/imap.rb, line 264
def open_attachment(mail)
  file = mail.attachments.detect { |part| validate_attachment(part) }
  validate_file(file) if file or return
  yield file.decoded if valid?
  file
end
open_message(id) { |mail| ... } click to toggle source
# File lib/stockboy/providers/imap.rb, line 254
def open_message(id)
  return unless id
  client do |imap|
    imap_message = imap.fetch(id, 'RFC822').first or return
    mail = ::Mail.new(imap_message.attr['RFC822'])
    yield mail if block_given?
    mail
  end
end
picked_message_key?() click to toggle source
# File lib/stockboy/providers/imap.rb, line 278
def picked_message_key?
  !!@message_key
end
read_data_size(data_file) click to toggle source
# File lib/stockboy/providers/imap.rb, line 321
def read_data_size(data_file)
  @data_size ||= data_file.body.raw_source.bytesize
end
validate() click to toggle source
# File lib/stockboy/providers/imap.rb, line 271
def validate
  errors << "host must be specified" if host.blank?
  errors << "username must be specified" if username.blank?
  errors << "password must be specified" if password.blank?
  errors.empty?
end
validate_attachment(part) click to toggle source
# File lib/stockboy/providers/imap.rb, line 282
def validate_attachment(part)
  case attachment
  when String
    part.filename == attachment
  when Regexp
    part.filename =~ attachment
  else
    true
  end
end
validate_file(data_file) click to toggle source
# File lib/stockboy/providers/imap.rb, line 301
def validate_file(data_file)
  return errors << "No matching attachments" unless data_file
  validate_file_smaller(data_file)
  validate_file_larger(data_file)
end
validate_file_larger(data_file) click to toggle source
# File lib/stockboy/providers/imap.rb, line 314
def validate_file_larger(data_file)
  read_data_size(data_file)
  if file_larger && data_size < file_larger
    errors << "File size smaller than #{file_larger}"
  end
end
validate_file_smaller(data_file) click to toggle source
# File lib/stockboy/providers/imap.rb, line 307
def validate_file_smaller(data_file)
  read_data_size(data_file)
  if file_smaller && data_size > file_smaller
    errors << "File size larger than #{file_smaller}"
  end
end