class FatFreeCRM::MailProcessor::Base

Constants

KEYWORDS

Public Class Methods

new() click to toggle source
# File lib/fat_free_crm/mail_processor/base.rb, line 20
def initialize
  @archived = 0
  @discarded = 0
  @dry_run = false
end

Public Instance Methods

run(dry_run = false) click to toggle source
# File lib/fat_free_crm/mail_processor/base.rb, line 52
def run(dry_run = false)
  log "Not discarding or archiving any new messages..." if @dry_run = dry_run
  connect! || (return nil)
  with_new_emails do |uid, email|
    # Subclasses must define a #process method that takes arguments: uid, email
    process(uid, email)
    archive(uid)
  end
ensure
  log "messages processed=#{@archived + @discarded} archived=#{@archived} discarded=#{@discarded}"
  disconnect!
end
setup() click to toggle source

Setup imap folders in settings.

# File lib/fat_free_crm/mail_processor/base.rb, line 28
def setup
  log "connecting to #{@settings[:server]}..."
  connect!(setup: true) || (return nil)
  log "logged in to #{@settings[:server]}, checking folders..."
  folders = [@settings[:scan_folder]]
  folders << @settings[:move_to_folder] unless @settings[:move_to_folder].blank?
  folders << @settings[:move_invalid_to_folder] unless @settings[:move_invalid_to_folder].blank?

  # Open (or create) destination folder in read-write mode.
  folders.each do |folder|
    if @imap.list("", folder)
      log "folder #{folder} OK"
    else
      log "folder #{folder} missing, creating..."
      @imap.create(folder)
    end
  end
rescue Exception => e
  warn "setup error #{e.inspect}"
ensure
  disconnect!
end

Private Instance Methods

archive(uid) click to toggle source

Archive message (valid) action based on settings

# File lib/fat_free_crm/mail_processor/base.rb, line 136
def archive(uid)
  if @dry_run
    log "Not archiving message"
  else
    @imap.uid_copy(uid, @settings[:move_to_folder]) if @settings[:move_to_folder]
    @imap.uid_store(uid, "+FLAGS", [:Seen])
  end
  @archived += 1
end
clean_invalid_utf8_bytes(text, src_encoding) click to toggle source

Forces the encoding of the given string to UTF8 and replaces invalid characters. This is necessary as emails sometimes have invalid characters like MS “Smart Quotes.”

# File lib/fat_free_crm/mail_processor/base.rb, line 219
def clean_invalid_utf8_bytes(text, src_encoding)
  text.encode(
    'UTF-8',
    src_encoding,
    invalid: :replace,
    undef: :replace,
    replace: ''
  )
end
connect!(options = {}) click to toggle source

Connects to the imap server with the loaded settings

# File lib/fat_free_crm/mail_processor/base.rb, line 69
def connect!(options = {})
  log "connecting & logging in to #{@settings[:server]}..."
  @imap = Net::IMAP.new(@settings[:server], @settings[:port], @settings[:ssl])
  @imap.login(@settings[:user], @settings[:password])
  log "logged in to #{@settings[:server]}, checking folders..."
  @imap.select(@settings[:scan_folder]) unless options[:setup]
  @imap
rescue Exception => e
  warn "Could not login to the IMAP server: #{e.inspect}" unless Rails.env == "test"
  nil
end
discard(uid) click to toggle source

Discard message (not valid) action based on settings

# File lib/fat_free_crm/mail_processor/base.rb, line 124
def discard(uid)
  if @dry_run
    log "Not discarding message"
  else
    @imap.uid_copy(uid, @settings[:move_invalid_to_folder]) if @settings[:move_invalid_to_folder]
    @imap.uid_store(uid, "+FLAGS", [:Deleted])
  end
  @discarded += 1
end
disconnect!() click to toggle source
# File lib/fat_free_crm/mail_processor/base.rb, line 82
def disconnect!
  if @imap
    @imap.logout
    unless @imap.disconnected?
      begin
        @imap.disconnect
      rescue Exception
        nil
      end
    end
  end
end
find_sender(email_address) click to toggle source
# File lib/fat_free_crm/mail_processor/base.rb, line 162
def find_sender(email_address)
  if @sender = User.find_by(
    '(lower(email) = :email OR lower(alt_email) = :email) AND suspended_at IS NULL',
    email: email_address.downcase
  )
    # Set the PaperTrail user for versions (if user is found)
    PaperTrail.request.whodunnit = @sender.id.to_s
  end
end
is_valid?(email) click to toggle source
# File lib/fat_free_crm/mail_processor/base.rb, line 147
def is_valid?(email)
  valid = email.content_type != "text/html"
  log("not a text message, discarding") unless valid
  valid
end
log(message, email = nil) click to toggle source

Centralized logging.

# File lib/fat_free_crm/mail_processor/base.rb, line 183
def log(message, email = nil)
  unless %w[test cucumber].include?(Rails.env)
    klass = self.class.to_s.split("::").last
    klass << " [Dry Run]" if @dry_run
    puts "[#{Time.now.rfc822}] #{klass}: #{message}"
    puts "[#{Time.now.rfc822}] #{klass}: From: #{email.from}, Subject: #{email.subject} (#{email.message_id})" if email
  end
end
plain_text_body(email) click to toggle source

Returns the plain-text version of an email, or strips html tags if only html is present.

# File lib/fat_free_crm/mail_processor/base.rb, line 195
def plain_text_body(email)
  # Extract all parts including nested
  parts = if email.multipart?
            email.parts.map { |p| p.multipart? ? p.parts : p }.flatten
          else
            charset = email.charset
            [email]
  end

  if text_part = parts.detect { |p| p.content_type.include?('text/plain') }
    text_body = text_part.body.to_s
    charset = text_part.charset if email.multipart?
  else
    html_part = parts.detect { |p| p.content_type.include?('text/html') } || email
    text_body = Premailer.new(html_part.body.to_s, with_html_string: true).to_plain_text
    charset = html_part.charset if email.multipart?
  end

  # Convert to UTF8 and standardize newline
  clean_invalid_utf8_bytes(text_body.strip.gsub("\r\n", "\n"), charset)
end
sender_has_permissions_for?(asset) click to toggle source
# File lib/fat_free_crm/mail_processor/base.rb, line 173
def sender_has_permissions_for?(asset)
  return true if asset.access == "Public"
  return true if asset.user_id == @sender.id || asset.assigned_to == @sender.id
  return true if asset.access == "Shared" && Permission.exists('user_id = ? AND asset_id = ? AND asset_type = ?', @sender.id, asset.id, asset.class.to_s)

  false
end
sent_from_known_user?(email) click to toggle source
# File lib/fat_free_crm/mail_processor/base.rb, line 154
def sent_from_known_user?(email)
  email_address = email.from.first
  known = !find_sender(email_address).nil?
  log("sent by unknown user #{email_address}, discarding") unless known
  known
end
with_new_emails() { |uid, email| ... } click to toggle source
# File lib/fat_free_crm/mail_processor/base.rb, line 96
def with_new_emails
  @imap.uid_search(%w[NOT SEEN]).each do |uid|
    begin
      email = Mail.new(@imap.uid_fetch(uid, 'RFC822').first.attr['RFC822'])
      log "fetched new message...", email
      if is_valid?(email) && sent_from_known_user?(email)
        yield(uid, email)
      else
        discard(uid)
      end
    rescue Exception => e
      if %w[test development].include?(Rails.env)
        warn e
        warn e.backtrace
      end
      log "error processing email: #{e.inspect}", email
      discard(uid)
    end

    if @dry_run
      log "Marking message as unread"
      @imap.uid_store(uid, "-FLAGS", [:Seen])
    end
  end
end