require 'rails-deprecated_sanitizer'

class EasyMailHandler < MailHandler
  attr_reader :instance_options

  def self.receive(raw_mail, options={})
    easy_handler_options = {
      :easy_rake_task => options[:easy_rake_task],
      :easy_rake_task_info_detail => options[:easy_rake_task_info_detail]
    }
    options = options.deep_dup

    options[:issue] ||= {}

    options[:allow_override] ||= []
    if options[:allow_override].is_a?(String)
      options[:allow_override] = options[:allow_override].split(',')
    end
    options[:allow_override].map! { |s| s.strip.downcase.gsub(/\s+/, '_') }
    # Project needs to be overridable if not specified
    options[:allow_override] << 'project' unless options[:issue].has_key?(:project)

    options[:no_account_notice] = (options[:no_account_notice].to_s == '1')
    options[:no_notification] = (options[:no_notification].to_s == '1')
    options[:no_permission_check] = (options[:no_permission_check].to_s == '1')

    raw_mail.force_encoding('ASCII-8BIT')

    ActiveSupport::Notifications.instrument('receive.action_mailer') do |payload|
      mail = Mail.new(raw_mail)
      set_payload_for_mail(payload, mail)
      new.receive(mail, options.merge!(easy_handler_options))
    end
  end

  def receive(email, options={})
    @handler_options = options
    @email = email
    sender_email = Array(email.from).first.to_s.strip

    # Ignore emails received from the application emission address to avoid hell cycles
    if sender_email.casecmp(Setting.mail_from.to_s.strip) == 0
      log_info_msg "#{self.class.name}: ignoring email from Redmine emission address [#{sender_email}]"
      return false
    end

    if sender_email.to_s.strip.downcase =~ /\A.*MAILER-DAEMON@.*\z/
      log_info_msg "#{self.class.name}: ignoring email from MAILER-DAEMON address [#{sender_email}]"
      return false
    end

    if handler_options.key?(:skip_ignored_emails_headers_check) && handler_options[:skip_ignored_emails_headers_check] == '1'
    else
      # Ignore auto generated emails
      self.class.ignored_emails_headers.each do |key, ignored_value|
        value = email.header[key]
        if value
          value = value.to_s.downcase
          if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
            log_info_msg "#{self.class.name}: ignoring email with #{key}:#{value} header"
            return false
          end
        end
      end

      # TODO add to ignored_emails_headers like (\s*[2-9])
      if email.header['X-Loop-Detect'].to_s.to_i > 1
        log_info_msg "#{self.class.name}: ignoring email with X-Loop-Detect bigger than 1"
        return false
      end
    end

    @user = User.having_mail(sender_email).first if sender_email.present?
    if @user && !@user.active?
      case handler_options[:unknown_user]
      when 'accept'
        @user = User.anonymous
      else
        @user = nil
      end
    end

    if @user.nil?
      # Email was submitted by an unknown user
      case handler_options[:unknown_user]
      when 'accept'
        @user = User.anonymous
      when 'create'
        @user = create_user_from_email
        if @user
          log_info_msg "#{self.class.name}: [#{@user.login}] account created"
          add_user_to_group(handler_options[:default_group])
          unless handler_options[:no_account_notice]
            Mailer.account_information(@user, @user.password).deliver
          end
        else
          log_error_msg "#{self.class.name}: could not create account for [#{sender_email}]"
          return false
        end
      else
        @user = User.where(:login => handler_options[:unknown_user]).first unless handler_options[:unknown_user].blank?
        if @user.nil?
          # Default behaviour, emails from unknown users are ignored
          log_info_msg "#{self.class.name}: ignoring email from unknown user [#{sender_email}]"
          return false
        end
      end
    end
    User.current = @user

    obj = dispatch

    if obj.is_a?(ActiveRecord::Base) && (easy_rake_task_info_detail = handler_options[:easy_rake_task_info_detail])
      easy_rake_task_info_detail.entity = obj
      easy_rake_task_info_detail.save
    end

    obj
  end

  def dispatch
    headers = [email.in_reply_to, email.references].flatten.compact
    subject = Redmine::CodesetUtil.replace_invalid_utf8(cleaned_up_subject).to_s
    if headers.detect { |h| h.to_s =~ MESSAGE_ID_RE }
      klass, object_id = $1, $2.to_i
      method_name = "receive_#{klass}_reply"
      if respond_to?(method_name.to_sym, true)
        result = send method_name, object_id
        unless result
          raise MissingInformation.new("Unable to determine target #{klass} id: #{object_id}")
        end
        result
      else
        # ignoring it
        log_error_msg "#{self.class.name}: cannot find method #{method_name} from email"
        false
      end
    elsif m = subject.match(ISSUE_REPLY_SUBJECT_RE)
      unless receive_issue_reply(m[1].to_i)
        raise MissingInformation.new("Unable to determine target issue id: #{m[1].to_i}")
      end
      m
    elsif m = subject.match(MESSAGE_REPLY_SUBJECT_RE)
      unless receive_message_reply(m[1].to_i)
        raise MissingInformation.new("Unable to determine target message id: #{m[1].to_i}")
      end
      m
    else
      dispatch_to_default
    end
  rescue ActiveRecord::RecordInvalid => e
    # TODO: send a email to the user
    log_error_msg "#{self.class.name}: #{e.message}"
    false
  rescue MissingInformation => e
    log_error_msg "#{self.class.name}: missing information from #{user}: #{e.message}"
    false
  rescue UnauthorizedAction => e
    log_error_msg "#{self.class.name}: unauthorized attempt from #{user}"
    false
  end

  def save_email_as_eml(entity)
    filename = Redmine::CodesetUtil.replace_invalid_utf8(cleaned_up_subject).to_s.tr(' ', '_')
    EasyUtils::FileUtils.save_and_attach_email(self.email, entity, filename, User.current)
  end

  def cleaned_up_subject
    super.presence || '(no subject)'
  end

  def stripped_plain_text_body
    return @stripped_plain_text_body unless @stripped_plain_text_body.nil?

    part = self.email.text_part || self.email.html_part
    part ||= self.email if self.email.text?
    if part
      body = part.body.decoded
      encoding = pick_encoding(part)
      @stripped_plain_text_body = begin
        convert_to_utf8(body, encoding)
      rescue *Redmine::CodesetUtil::ENCODING_EXCEPTIONS
        Rails.logger.warn "ENCODING #{encoding} isn't supported"
        Redmine::CodesetUtil.replace_invalid_utf8(body)
      end

      # strip html tags and remove doctype directive
      @stripped_plain_text_body = strip_tags(@stripped_plain_text_body.strip)
      @stripped_plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
    else
      @stripped_plain_text_body = ''
    end
    @stripped_plain_text_body
  end

  def add_attachments(obj)
    fix_attached_images_broken_filename

    if self.email.attachments && self.email.attachments.any?
      self.email.attachments.each do |attachment|
        next unless accept_attachment?(attachment)

        create_or_update_attachment(obj, attachment)
      end
    end
  end

  # accept all attachments for rdm_mailhandler.rb cause the original eml isn't available
  #def accept_attachment?(attachment)
  #  !attachment.inline? && super(attachment)
  #end

  def create_or_update_attachment(obj, attachment)
    attachment_or_nothing = obj.get_existing_version(attachment.filename, {})

    if attachment_or_nothing
      attachment_or_nothing.attributes = {
          :file => attachment.body.decoded,
          :container => obj,
          :author => self.user
          }
      attachment_or_nothing.description ||= attachment.filename if attachment_or_nothing.description_required?
      attachment_or_nothing.files_to_final_location
      attachment_or_nothing.save
      obj.after_new_version_create_journal(attachment_or_nothing)
    else
      attachment_or_nothing = obj.attachments.build(
        :file => attachment.body.decoded,
        :filename => attachment.filename,
        :author => self.user,
        :content_type => attachment.mime_type
      )
      attachment_or_nothing.description = attachment.filename if attachment_or_nothing.description_required?
      attachment_or_nothing.save
    end
  end

  def fix_attached_images_broken_filename
    self.email.all_parts.each do |part|
      if part.mime_type =~ /^image\/([a-z\-]+)$/
        file_extension = $1

        if part.filename.present? && part.filename =~ /\.$/
          # add missing file extension
          part.content_type = part.content_type.gsub(/name=\"?.+\./, "\\0#{file_extension}") if part.content_type
          part.content_disposition = part.content_disposition.gsub(/filename=\"?.+\./, "\\0#{file_extension}") if part.content_disposition #(fixed_disposition)

        elsif part.filename.blank? && part.content_id.present? && part.content_id =~ /([a-z0-9\.]+)@/
          # create missing filename
          part.content_disposition = "inline; filename=\"#{$1}.#{file_extension}\""
        end
      end
    end
  end

  # Destructively extracts the value for +attr+ in +text+
  # Returns nil if no matching keyword found
  def extract_keyword!(text, attr, format=nil)
    keys = [attr.to_s.humanize]
    if attr.is_a?(Symbol)
      keys << l("field_#{attr}", :default => '', :locale => self.user.language) if self.user && self.user.language.present?
      keys << l("field_#{attr}", :default => '', :locale => Setting.default_language) if Setting.default_language.present?
    end
    keys.reject! { |k| k.blank? }
    keys.collect! { |k| Regexp.escape(k) }
    additional_keys = []
    keys.each do |key|
      key_without_diacritics = key.mb_chars.normalize(:kd).gsub(/[^\x00-\x7F]/n, '').to_s
      additional_keys << key_without_diacritics
      additional_keys << key_without_diacritics.downcase
      additional_keys << key_without_diacritics.upcase
      additional_keys << key.downcase
      additional_keys << key.upcase
    end
    keys.concat(additional_keys)
    keys.uniq!
    format ||= '.+'
    regexp = /^[ ]*(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i
    if m = text.match(regexp)
      keyword = m[2].strip
      text.sub!(regexp, '')
    end
    keyword
  end

  def get_keyword(attr, options={})
    @keywords ||= {}
    if @keywords.has_key?(attr)
      @keywords[attr]
    else
      @keywords[attr] = begin
        override = options.key?(:override) ?
          options[:override] :
          (handler_options[:allow_override] & [attr.to_s.downcase.gsub(/\s+/, '_'), 'all']).present?

        if override && (v = extract_keyword!(stripped_plain_text_body, attr, options[:format]))
          v
        elsif !handler_options[:issue][attr].blank?
          handler_options[:issue][attr]
        end
      end
    end
  end

  def plain_text_body
    return @plain_text_body unless @plain_text_body.nil?
    if Setting.text_formatting == 'HTML'
      html_part, text_part, no_mime_part = false, false, false
      if (text_parts = self.email.all_parts.select { |p| p.mime_type == 'text/plain' }).present?
        parts = text_parts
        text_part = true
      elsif (html_parts = self.email.all_parts.select { |p| p.mime_type == 'text/html' }).present?
        parts = html_parts
        html_part = true
      else
        parts = self.email.text? ? [self.email] : []
        html_part = true
        no_mime_part = true
      end

      parts.reject! do |part|
        part.attachment?
      end

      @plain_text_body = parts.map do |p|
        body = p.body.decoded
        encoding = pick_encoding(p)
        encoded_body = begin
          convert_to_utf8(body, encoding)
        rescue *Redmine::CodesetUtil::ENCODING_EXCEPTIONS
          Rails.logger.warn "ENCODING #{encoding} isn't supported"
          Redmine::CodesetUtil.replace_invalid_utf8(body)
        end
        # convert html parts to text
        p.mime_type == 'text/html' ? self.class.html_body_to_text(encoded_body) : self.class.plain_text_body_to_text(encoded_body)
      end.join("\r\n")

      if (!html_part && text_part) || (!html_part && !text_part) || no_mime_part
        @plain_text_body.gsub!(/\n/, '<br />')
      end
    else
      parts = if (text_parts = email.all_parts.select {|p| p.mime_type == 'text/plain'}).present?
        text_parts
      elsif (html_parts = email.all_parts.select {|p| p.mime_type == 'text/html'}).present?
        html_parts
      else
        email.text? ? [email] : []
      end

      parts.reject! do |part|
        part.attachment?
      end

      @plain_text_body = parts.map do |p|
        body = p.body.decoded
        encoding = pick_encoding(p)
        encoded_body = begin
          convert_to_utf8(body, encoding)
        rescue *Redmine::CodesetUtil::ENCODING_EXCEPTIONS
          Rails.logger.warn "ENCODING #{encoding} isn't supported"
          Redmine::CodesetUtil.replace_invalid_utf8(body)
        end

        # convert html parts to text
        p.mime_type == 'text/html' ? self.class.html_body_to_text(encoded_body) : self.class.plain_text_body_to_text(encoded_body)
      end.join("\r\n")
    end
    @plain_text_body
  end

  def cleanup_body(body)
    cleanup_body = body.dup
    delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map { |s| Regexp.escape(s) }
    unless delimiters.empty?
      regex = Regexp.new("^*(#{ delimiters.join('|') }).*", Regexp::MULTILINE)
      cleanup_body = body.gsub(regex, '')
    end
    cleanup_body = cleanup_body.strip
    return cleanup_body unless Setting.text_formatting == 'HTML'

    parse_body = Nokogiri::HTML.parse(cleanup_body)
    ['head', 'meta', 'style', 'script', 'base'].each do |trash|
      parse_body.search(trash).remove
    end
    # remove blank p, that create empty lines.
    parse_body.css('p').each { |p| p.remove if p.content.strip.blank? }

    body_html = parse_body.at('body')

    msg = '<div class="easy_long_note"><div>'
    msg << (body_html.present? ? body_html.inner_html : parse_body.text)
    msg << '</div></div>'

    return msg
  end

  protected

  def log_info_msg(err_msg)
    if handler_options[:logger]
      handler_options[:logger].info(err_msg)
    elsif logger
      logger.info(err_msg)
    end
    update_easy_rake_task_info_detail(err_msg)
  end

  def log_error_msg(err_msg)
    if handler_options[:logger]
      handler_options[:logger].error(err_msg)
    elsif logger
      logger.error(err_msg)
    end
    update_easy_rake_task_info_detail(err_msg)
  end

  def update_easy_rake_task_info_detail(err_msg)
    if handler_options[:easy_rake_task_info_detail]
      handler_options[:easy_rake_task_info_detail].update_column(:detail, err_msg)
    end
  end

  def mails_from_and_cc_array(email)
    return [] if email.nil?

    mails = []
    mails.concat(Array.wrap(email.reply_to)) if !email.reply_to.blank?
    mails.concat(Array.wrap(email.from)) if !email.from.blank?
    mails.concat(Array.wrap(email.cc)) if !email.cc.blank?

    mails.flatten.reject(&:blank?).collect { |mail| mail.to_s.strip.downcase }.uniq
  end

  def mails_from_and_cc(email)
    mails_from_and_cc_array(email).join(', ')
  end

  def pick_encoding(part)
    Mail::RubyVer.respond_to?(:pick_encoding) ? Mail::RubyVer.pick_encoding(part.charset).to_s : part.charset
  end

  def convert_to_utf8(str, encoding)
    if !str.nil? && encoding.to_s.downcase == 'utf-7' && Net::IMAP.respond_to?(:decode_utf7)
      str.force_encoding('UTF-8')
      Redmine::CodesetUtil.to_utf8(Net::IMAP.decode_utf7(str), 'UTF-8')
    else
      Redmine::CodesetUtil.to_utf8(str, encoding)
    end
  end

end
