class Clacks::Service

Constants

WATCHDOG_SLEEP

In practice timeouts occur when there is no activity keeping an IMAP connection open. Timeouts occuring are:

IMAP server timeout: typically after 30 minutes with no activity.
NAT Gateway timeout: typically after 15 minutes with an idle connection.

The solution to this is for the IMAP client to issue a NOOP (No Operation) command at intervals, typically every 29 minutes. We choose default 10 minutes.

Public Instance Methods

run() click to toggle source
# File lib/clacks/service.rb, line 16
def run
  begin
    Clacks.logger.info "Clacks v#{Clacks::VERSION} started"
    if Clacks.config[:pop3]
      run_pop3
    elsif Clacks.config[:imap]
      run_imap
    else
      raise "Either a POP3 or an IMAP server must be configured"
    end
  rescue Exception => e
    fatal(e)
  end
end
stop() click to toggle source
# File lib/clacks/service.rb, line 31
def stop
  $STOPPING = true
  exit unless finding?
end

Private Instance Methods

fatal(e) click to toggle source
# File lib/clacks/service.rb, line 38
def fatal(e)
  unless e.is_a?(SystemExit) || e.is_a?(SignalException)
    Clacks.logger.fatal("#{e.message} (#{e.class})\n#{(e.backtrace || []).join("\n")}")
  end
  stop
  raise e
end
finding() { || ... } click to toggle source
# File lib/clacks/service.rb, line 232
def finding(&block)
  @finding = true
  yield
ensure
  @finding = false
end
finding?() click to toggle source
# File lib/clacks/service.rb, line 239
def finding?
  @finding
end
imap_find(imap) click to toggle source

Keep processing emails until nothing is found anymore, or until a QUIT signal is received to stop the process.

# File lib/clacks/service.rb, line 154
def imap_find(imap)
  options = Clacks.config[:find_options]
  delete_after_find = options[:delete_after_find]
  begin
    break if stopping?
    uids = imap.uid_search(options[:keys] || 'ALL')
    uids.reverse! if options[:what].to_sym == :last
    uids = uids.first(options[:count]) if options[:count].is_a?(Integer)
    uids.reverse! if (options[:what].to_sym == :last && options[:order].to_sym == :asc) ||
                     (options[:what].to_sym != :last && options[:order].to_sym == :desc)
    processed = 0
    expunge = false
    uids.each do |uid|
      break if stopping?
      source = imap.uid_fetch(uid, ['RFC822']).first.attr['RFC822']
      mail = nil
      begin
        mail = Mail.new(source)
        mail.mark_for_delete = true if delete_after_find
        Clacks.config[:on_mail].call(mail)
      rescue StandardError => e
        Clacks.logger.error(e.message)
        Clacks.logger.error(e.backtrace)
      end
      begin
        imap.uid_copy(uid, options[:archivebox]) if options[:archivebox]
        if delete_after_find && (mail.nil? || mail.is_marked_for_delete?)
          expunge = true
          imap.uid_store(uid, "+FLAGS", [Net::IMAP::DELETED])
        end
      rescue StandardError => e
        Clacks.logger.error(e.message)
      end
      processed += 1
    end
    imap.expunge if expunge
  end while uids.any? && processed == uids.length
end
imap_idle_support?(processor) click to toggle source
# File lib/clacks/service.rb, line 88
def imap_idle_support?(processor)
  processor.connection { |imap| imap.capability.include?("IDLE") }
end
imap_idling(processor) click to toggle source
# File lib/clacks/service.rb, line 92
def imap_idling(processor)
  imap_watchdog
  loop do
    begin
      processor.connection do |imap|
        @imap = imap
        # select the mailbox to process
        imap.select(Clacks.config[:find_options][:mailbox])
        loop {
          break if stopping?
          finding { imap_find(imap) }
          # http://tools.ietf.org/rfc/rfc2177.txt
          Clacks.logger.debug('imap.idle start')
          imap.idle do |r|
            Clacks.logger.debug('imap.idle yields')
            if r.instance_of?(Net::IMAP::UntaggedResponse) && r.name == 'EXISTS'
              imap.idle_done unless r.data == 0
            elsif r.instance_of?(Net::IMAP::ContinuationRequest)
              Clacks.logger.info(r.data.text)
            end
          end
        }
      end
    rescue Net::IMAP::BadResponseError => e
      unless e.message == 'Could not parse command'
        Clacks.logger.error("#{e.message} (#{e.class})\n#{(e.backtrace || []).join("\n")}")
      end
      # reconnect in next loop
    rescue Net::IMAP::Error, IOError => e
      # OK: reconnect in next loop
    rescue Errno::ECONNRESET => e
      # Connection reset by peer: reconnect in next loop
    rescue StandardError => e
      Clacks.logger.error("#{e.message} (#{e.class})\n#{(e.backtrace || []).join("\n")}")
      sleep(5) unless stopping?
    end
    break if stopping?
  end
end
imap_validate_options(options) click to toggle source

Follows mostly the defaults from the Mail gem

# File lib/clacks/service.rb, line 73
def imap_validate_options(options)
  options ||= {}
  options[:mailbox] ||= 'INBOX'
  options[:count]   ||= 5
  options[:order]   ||= :asc
  options[:what]    ||= :first
  options[:keys]    ||= 'ALL'
  options[:delete_after_find] ||= false
  options[:mailbox] = Net::IMAP.encode_utf7(options[:mailbox])
  if options[:archivebox]
    options[:archivebox] = Net::IMAP.encode_utf7(options[:archivebox])
  end
  options
end
imap_watchdog() click to toggle source

tools.ietf.org/rfc/rfc2177.txt

# File lib/clacks/service.rb, line 133
def imap_watchdog
  Thread.new do
    loop do
      begin
        Clacks.logger.debug('watchdog sleeps')
        sleep(WATCHDOG_SLEEP)
        Clacks.logger.debug('watchdog woke up')
        @imap.idle_done
        Clacks.logger.debug('watchdog signalled idle process')
      rescue StandardError => e
        Clacks.logger.debug { "watchdog received error: #{e.message} (#{e.class})\n#{(e.backtrace || []).join("\n")}" }
        # noop
      rescue Exception => e
        fatal(e)
      end
    end
  end
end
poll(processor) click to toggle source
# File lib/clacks/service.rb, line 193
def poll(processor)
  polling_msg = if polling?
    "Clacks polling every #{poll_interval} seconds."
  else
    "Clacks polling for messages once."
  end
  Clacks.logger.info(polling_msg)

  find_options = Clacks.config[:find_options]
  on_mail = Clacks.config[:on_mail]
  loop do
    break if stopping?
    finding {
      processor.find(find_options) do |mail|
        if stopping?
          mail.skip_deletion
        else
          begin
            on_mail.call(mail)
          rescue StandardError => e
            Clacks.logger.error(e.message)
            Clacks.logger.error(e.backtrace)
          end
        end
      end
    }
    break if stopping? || !polling?
    sleep(poll_interval)
  end
end
poll_interval() click to toggle source
# File lib/clacks/service.rb, line 224
def poll_interval
  Clacks.config[:poll_interval]
end
polling?() click to toggle source
# File lib/clacks/service.rb, line 228
def polling?
  poll_interval > 0
end
run_imap() click to toggle source
# File lib/clacks/service.rb, line 54
def run_imap
  config = Clacks.config[:imap]
  options = Clacks.config[:find_options]
  processor = Mail::IMAP.new(config)
  if $DEBUG
    Net::IMAP.debug = true
    Clacks.logger.level = Logger::DEBUG
  end
  imap_validate_options(options)
  if imap_idle_support?(processor)
    Clacks.logger.info("Clacks IMAP idling #{config[:user_name]}@#{config[:address]}")
    imap_idling(processor)
  else
    Clacks.logger.info("Clacks IMAP polling #{config[:user_name]}@#{config[:address]}")
    poll(processor)
  end
end
run_pop3() click to toggle source
# File lib/clacks/service.rb, line 46
def run_pop3
  config = Clacks.config[:pop3]
  Clacks.logger.info("Clacks POP3 polling #{config[:user_name]}@#{config[:address]}")
  # TODO: if $DEBUG
  processor = Mail::IMAP.new(config)
  poll(processor)
end
stopping?() click to toggle source
# File lib/clacks/service.rb, line 243
def stopping?
  $STOPPING
end