ari/jobsworth

View on GitHub
app/models/mailman.rb

Summary

Maintainability
D
1 day
Test Coverage
# encoding: UTF-8
# Receive and handle emails sent to tasks

class Mailman < ActionMailer::Base
  # The marker in the email body that shows where the new content ends
  BODY_SPLIT = 'o------ please reply above this line ------o'

  def self.receive(mail)
    # fix invalid byte sequence in UTF-8
    # https://github.com/mikel/mail/issues/340
    mail.force_encoding('binary')

    super
  end

  ### Mailman::Email provides a way to extract content from incoming email
  class Email
    attr_accessor :from, :body, :subject, :user, :company, :email_address, :email

    def initialize(email)
      @from = email.from.first
      @body = Email.get_body(email)
      @subject = email.subject
      @email = email

      # find company
      (email.to+Array.wrap(email.resent_to)).each do |to|
        next unless to.include?(Setting.domain)
        subdomain = to.split('@')[1].split('.')[0]
        @company ||= Company.find_by(:subdomain => subdomain)
      end

      # if company not found but we're using a single company install, just use that one
      @company ||= Company.first if Company.count == 1

      # backward compatibility: there may be bad data in db
      @email_address = EmailAddress.where('user_id IS NOT NULL').where(:email => @from).first
      @email_address = EmailAddress.where(:email => @from).first unless @email_address
      @email_address = EmailAddress.create(:email => @from, :company => company) unless @email_address

      # find user
      @user = @email_address.user
    end

    def blank?
      @body.blank?
    end

    def bad_subject?
      @subject.strip! unless @subject.nil?
      return true if @subject.blank?
      BAD_SUBJECTS.include?(@subject)
    end

    def too_large?
      @email.attachments.detect { |file| file.body.to_s.size > MAX_ATTACHMENT_SIZE }
    end

    def too_old?
      @email.date < (Time.now - 1.week)
    end

    def self.get_body(email)
      body = nil
      if email.multipart?
        email.parts.each do |m|
          next if body

          if m.content_type =~ /text\/plain/i
            body = m.body.to_s.force_encoding(m.charset || 'US-ASCII').encode(Encoding.default_internal)
          elsif m.multipart?
            body = get_body(m)
          end
        end
      end

      body ||= email.body.to_s.force_encoding(email.charset || 'US-ASCII').encode(Encoding.default_internal)
      body = Email.clean_body(body)
      return body
    end

    # helper method to remove email reply noise from the body
    def self.clean_body(body)
      new_body_end = body.to_s.index(Mailman::BODY_SPLIT) || body.to_s.length
      body = body.to_s[0, new_body_end].strip

      lines = body.to_s.split("\n")
      while lines.any?
        line = lines.last.strip

        if line.blank? or line.match(/^[<>]+$/) or line.match(/.* wrote:/)
          lines.pop
        else
          break
        end
      end

      return lines.join("\n")
    end
  end
  ### end Mailman::Email

  def receive(email)
    # create wrapper email object
    wrapper = Mailman::Email.new(email)

    logger.tagged('EMAIL TRACKING') { logger.info 'receive wrapper' }
    logger.tagged('EMAIL TRACKING') { logger.info wrapper.inspect }
    logger.tagged('EMAIL TRACKING') { logger.info "company #{wrapper.company.inspect}" }
    logger.tagged('EMAIL TRACKING') { logger.info "user #{wrapper.user}" }

    # check invalid email
    response_line =
        if wrapper.blank?
          I18n.t('mailmans.wrapper.blank')
        elsif wrapper.too_large?
          I18n.t('mailmans.wrapper.too_large', max: MAX_ATTACHMENT_SIZE_HUMAN)
        elsif wrapper.too_old?
          I18n.t('mailmans.wrapper.too_old')
        elsif wrapper.bad_subject?
          I18n.t('mailmans.wrapper.bad_subject')
        end

    # if no company found
    unless wrapper.company
      response_line = I18n.t('mailmans.no_company')
    end

    if wrapper.user and !wrapper.user.active
      response_line = I18n.t('mailmans.inactive_user')
    end

    # find target
    target = target_for(email, wrapper.company)

    logger.tagged('EMAIL TRACKING') { logger.info 'receive target' }
    logger.tagged('EMAIL TRACKING') { logger.info target }

    unless target
      response_line= I18n.t('mailmans.no_related')
    end

    unless response_line.nil?
      Notifications.response_to_invalid_email(email.from.first, response_line).deliver
      return false
    end

    logger.tagged('EMAIL TRACKING') { logger.info 'receive response_line' }
    logger.tagged('EMAIL TRACKING') { logger.info response_line }

    if target.is_a?(TaskRecord)
      logger.tagged('EMAIL TRACKING') { logger.info 'Adding email to task' }
      add_email_to_task(wrapper, target)
    elsif target.is_a?(Project)
      logger.tagged('EMAIL TRACKING') { logger.info 'Creating new task from email' }
      create_task_from_email(wrapper, target)
    end

    wrapper
  end

  private

  # Returns the target location for the given email. Could be
  # a Task, a Project or nil.
  def target_for(email, company)
    logger.tagged('EMAIL TRACKING') { logger.info 'target_for email' }
    logger.tagged('EMAIL TRACKING') { logger.info email }

    logger.tagged('EMAIL TRACKING') { logger.info 'target_for company' }
    logger.tagged('EMAIL TRACKING') { logger.info company.inspect }

    target = nil
    (email.to+Array(email.resent_to)).each do |to|
      if to.include?('task-')
        _, task_num = /task-(\d+).*@.*/.match(to).to_a
        if task_num.to_i > 0
          target = TaskRecord.where('company_id = ? AND task_num = ?', company.id, task_num).first
        end
      end
    end

    target ||= default_project(company) if company

    target
  end

  # Returns the default email project for company, or nil
  # if none.
  def default_project(company)
    id = company.preference('incoming_email_project')
    logger.tagged('EMAIL TRACKING') { logger.info 'default_project company_id' }
    logger.tagged('EMAIL TRACKING') { logger.info id }
    return company.projects.find_by(:id => id)
  end

  def add_email_to_task(wrapper, task)
    files = save_attachments(wrapper, task)

    # if it's from unknown, add email to task email_addresses
    unless wrapper.user or task.email_addresses.include? wrapper.email_address
      task.email_addresses << wrapper.email_address
    end

    task.update_column(:updated_by_id, wrapper.email_address.id)
    task.touch

    work_log = WorkLog.create(
        :user => wrapper.user,
        :company => task.project.company,
        :project => task.project,
        :customer => task.project.customer,
        :email_address => wrapper.email_address,
        :task => task,
        :started_at => Time.now.utc,
        :duration => 0,
        :body => wrapper.body.first(16384)
    )

    logger.tagged('EMAIL TRACKING') { logger.info 'WorkLog:' }
    logger.tagged('EMAIL TRACKING') { logger.info work_log.inspect }

    if wrapper.user && wrapper.user.comment_private_by_default?
      work_log.update_column(:access_level_id, 2)
    end

    event_log = work_log.create_event_log(
        :user => wrapper.user,
        :event_type => EventLog::TASK_COMMENT,
        :company => work_log.company,
        :project => work_log.project
    )

    logger.tagged('EMAIL TRACKING') { logger.info 'EventLog:' }
    logger.tagged('EMAIL TRACKING') { logger.info event_log.inspect }

    notify_users(work_log, files)
    Trigger.fire(task, Trigger::Event::UPDATED)
  end

  def save_attachments(wrapper, task)
    wrapper.email.attachments.reject! { |a| a.filename =~ /signature\.asc|smime\.p7s/ }
    files = []
    files = wrapper.email.attachments.map do |attachment|
      add_attachment(wrapper, task, attachment)
    end
    return files.compact
  end

  def add_attachment(wrapper, task, attachment)
    Dir.mkdir(Rails.root.join('tmp')) unless Dir.exist?(Rails.root.join('tmp'))
    tempfile = File.open(Rails.root.join('tmp', attachment.filename.gsub(' ', '_').gsub(/[^a-zA-Z0-9_\.]/, '')), 'w')
    tempfile.write_nonblock(attachment.body)
    file= task.add_attachment(File.open(tempfile.path), wrapper.user)
    File.delete(tempfile.path) rescue 0 # ignore deletion error
    return file
  end

  def create_task_from_email(wrapper, project)
    task = TaskRecord.new(
        :name => wrapper.subject,
        :project => project,
        :company => project.company,
        :description => wrapper.body.first(16384),
        :duration => 0,
        :updated_by_id => wrapper.email_address.id
    )

    task.set_default_properties

    logger.tagged('EMAIL TRACKING') { logger.info 'New task:' }
    logger.tagged('EMAIL TRACKING') { logger.info task.inspect }

    begin
      task.save(:validate => false)
    rescue ActiveRecord::RecordNotUnique
      task.save(:validate => false)
    end

    logger.tagged('EMAIL TRACKING') { logger.info 'Saved task:' }
    logger.tagged('EMAIL TRACKING') { logger.info task.inspect }

    attach_users_to_task(task, wrapper.email)
    attach_customers_to_task(task)
    files = save_attachments(wrapper, task)

    work_log = WorkLog.create_task_created!(task, wrapper.user)
    work_log.email_address = wrapper.email_address
    work_log.save!

    notify_users(work_log, files)
    Trigger.fire(task, Trigger::Event::CREATED)
  end

  def attach_users_to_task(task, email)
    (Array(email.from) + Array(email.cc)).each do |email_addr|
      attach_user_or_email_address(email_addr, task, task.watchers)
    end
    (email.to || []).each do |email_addr|
      attach_user_or_email_address(email_addr, task, task.owners)
    end
  end

  def attach_user_or_email_address(email, task, users)
    user = task.project.company.users.active.by_email(email.strip).first
    if user
      users << user
    elsif !task.company.suppressed_emails.include?(email.strip)
      # backward compatibility: there may be bad data in db
      ea = EmailAddress.where('user_id IS NOT NULL').where(:email => email.strip).first
      ea = EmailAddress.where(:email => email.strip).first unless ea
      ea = EmailAddress.create(:email => email.strip, :company => task.company) unless ea
      task.email_addresses << ea
    end
  end

  def attach_customers_to_task(task)
    task.users.each do |user|
      if user.customer and !task.customers.include?(user.customer)
        task.customers << user.customer
        user.customer.users.auto_add.each do |u|
          task.watchers << u unless task.users.include?(u)
        end
      end
    end
    if task.customers.size.zero?
      task.customers << task.project.customer
    end
  end

  def notify_users(work_log, files)
    user = work_log.user
    tmp = user.receive_own_notifications
    user.receive_own_notifications = false
    #skip save! if incoming email came from unknown user
    if user.new_record?
      work_log.notify(files)
    else
      user.update_column(:receive_own_notifications, false)
      work_log.notify(files)
      user.update_column(:receive_own_notifications, tmp)
    end
  end

end