require 'stockboy/provider'
require 'net/imap'
require 'mail'
module Stockboy::Providers
class IMAP < Stockboy::Provider
require_relative 'imap/search_options'
dsl_attr :host
dsl_attr :username
dsl_attr :password
dsl_attr :mailbox
dsl_attr :subject
dsl_attr :from
dsl_attr :since, alias: :newer_than
dsl_attr :search
dsl_attr :attachment, alias: :file_name
dsl_attr :file_smaller, alias: :smaller_than
dsl_attr :file_larger, alias: :larger_than
dsl_attr :pick
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
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
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
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
def clear
super
@message_key = nil
@data_time = nil
@data_size = nil
end
def find_messages(options=nil)
client { |imap| imap.sort(['DATE'], search_keys(options), 'UTF-8') }
end
def search_keys(options=nil)
case options
when Array, String then options
else SearchOptions.new(options || default_search_options).to_imap
end
end
def default_search_options
{subject: subject, from: from, since: since}.reject { |k,v| v.nil? }
end
private
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
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
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
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
def picked_message_key?
!!@message_key
end
def validate_attachment(part)
case attachment
when String
part.filename == attachment
when Regexp
part.filename =~ attachment
else
true
end
end
def normalize_imap_datetime(datetime)
datetime.respond_to?(:getutc) ?
datetime.getutc.to_time : datetime.to_time.utc
end
def validate_file(data_file)
return errors << "No matching attachments" unless data_file
validate_file_smaller(data_file)
validate_file_larger(data_file)
end
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
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
def read_data_size(data_file)
@data_size ||= data_file.body.raw_source.bytesize
end
def inspect_message_key
"#{username}:#{host} message_uid #{message_key}"
end
end
end