190 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			Ruby
		
	
	
	
			
		
		
	
	
			190 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			Ruby
		
	
	
	
# Inspired in great part by Discourse's Email::Receiver
 | 
						|
module Gitlab
 | 
						|
  module Email
 | 
						|
    class Receiver
 | 
						|
      class ProcessingError < StandardError; end
 | 
						|
      class EmailUnparsableError < ProcessingError; end
 | 
						|
      class SentNotificationNotFoundError < ProcessingError; end
 | 
						|
      class ProjectNotFound < ProcessingError; end
 | 
						|
      class EmptyEmailError < ProcessingError; end
 | 
						|
      class AutoGeneratedEmailError < ProcessingError; end
 | 
						|
      class UserNotFoundError < ProcessingError; end
 | 
						|
      class UserBlockedError < ProcessingError; end
 | 
						|
      class UserNotAuthorizedError < ProcessingError; end
 | 
						|
      class NoteableNotFoundError < ProcessingError; end
 | 
						|
      class InvalidNoteError < ProcessingError; end
 | 
						|
      class InvalidIssueError < ProcessingError; end
 | 
						|
 | 
						|
      def initialize(raw)
 | 
						|
        @raw = raw
 | 
						|
      end
 | 
						|
 | 
						|
      def execute
 | 
						|
        raise EmptyEmailError if @raw.blank?
 | 
						|
 | 
						|
        if sent_notification
 | 
						|
          process_create_note
 | 
						|
 | 
						|
        elsif message_project
 | 
						|
          if message_sender.can?(:read_project, message_project)
 | 
						|
            process_create_issue
 | 
						|
          else # Must be private project without access
 | 
						|
            raise ProjectNotFound
 | 
						|
          end
 | 
						|
 | 
						|
        elsif reply_key =~ %r{/|\+}
 | 
						|
          # Sent Notification reply_key would not have / or +
 | 
						|
          raise ProjectNotFound
 | 
						|
        else
 | 
						|
          raise SentNotificationNotFoundError
 | 
						|
        end
 | 
						|
      end
 | 
						|
 | 
						|
      private
 | 
						|
      def process_create_note
 | 
						|
        raise AutoGeneratedEmailError if message.header.to_s =~ /auto-(generated|replied)/
 | 
						|
 | 
						|
        author = sent_notification.recipient
 | 
						|
        project = sent_notification.project
 | 
						|
 | 
						|
        validate_permission!(author, project, :create_note)
 | 
						|
 | 
						|
        raise NoteableNotFoundError unless sent_notification.noteable
 | 
						|
 | 
						|
        reply = process_reply(project)
 | 
						|
        raise EmptyEmailError if reply.blank?
 | 
						|
        note = create_note(reply)
 | 
						|
 | 
						|
        unless note.persisted?
 | 
						|
          msg = "The comment could not be created for the following reasons:"
 | 
						|
          note.errors.full_messages.each do |error|
 | 
						|
            msg << "\n\n- #{error}"
 | 
						|
          end
 | 
						|
 | 
						|
          raise InvalidNoteError, msg
 | 
						|
        end
 | 
						|
      end
 | 
						|
 | 
						|
      def process_create_issue
 | 
						|
        validate_permission!(message_sender, message_project, :create_issue)
 | 
						|
        validate_authentication_token!(message_sender)
 | 
						|
 | 
						|
        issue = Issues::CreateService.new(
 | 
						|
          message_project,
 | 
						|
          message_sender,
 | 
						|
          title:       message.subject,
 | 
						|
          description: process_reply(message_project)
 | 
						|
        ).execute
 | 
						|
 | 
						|
        unless issue.persisted?
 | 
						|
          msg = "The issue could not be created for the following reasons:"
 | 
						|
          issue.errors.full_messages.each do |error|
 | 
						|
            msg << "\n\n- #{error}"
 | 
						|
          end
 | 
						|
 | 
						|
          raise InvalidIssueError, msg
 | 
						|
        end
 | 
						|
      end
 | 
						|
 | 
						|
      def validate_permission!(author, project, permission)
 | 
						|
        raise UserNotFoundError unless author
 | 
						|
        raise UserBlockedError if author.blocked?
 | 
						|
        # TODO: Give project not found error if author cannot read project
 | 
						|
        raise UserNotAuthorizedError unless author.can?(permission, project)
 | 
						|
      end
 | 
						|
 | 
						|
      def validate_authentication_token!(author)
 | 
						|
        raise UserNotAuthorizedError unless author.authentication_token ==
 | 
						|
                                              authentication_token
 | 
						|
      end
 | 
						|
 | 
						|
      def message_sender
 | 
						|
        @message_sender ||= message.from.find do |email|
 | 
						|
          user = User.find_by_any_email(email)
 | 
						|
          break user if user
 | 
						|
        end
 | 
						|
      end
 | 
						|
 | 
						|
      def message_project
 | 
						|
        @message_project ||=
 | 
						|
          Project.find_with_namespace(project_namespace) if reply_key
 | 
						|
      end
 | 
						|
 | 
						|
      def process_reply(project)
 | 
						|
        reply = ReplyParser.new(message).execute.strip
 | 
						|
 | 
						|
        add_attachments(reply, project)
 | 
						|
 | 
						|
        reply
 | 
						|
      end
 | 
						|
 | 
						|
      def message
 | 
						|
        @message ||= Mail::Message.new(@raw)
 | 
						|
      rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError => e
 | 
						|
        raise EmailUnparsableError, e
 | 
						|
      end
 | 
						|
 | 
						|
      def reply_key
 | 
						|
        key_from_to_header || key_from_additional_headers
 | 
						|
      end
 | 
						|
 | 
						|
      def authentication_token
 | 
						|
        reply_key[/[^\+]+$/]
 | 
						|
      end
 | 
						|
 | 
						|
      def project_namespace
 | 
						|
        reply_key[/^[^\+]+/]
 | 
						|
      end
 | 
						|
 | 
						|
      def key_from_to_header
 | 
						|
        key = nil
 | 
						|
        message.to.each do |address|
 | 
						|
          key = Gitlab::IncomingEmail.key_from_address(address)
 | 
						|
          break if key
 | 
						|
        end
 | 
						|
 | 
						|
        key
 | 
						|
      end
 | 
						|
 | 
						|
      def key_from_additional_headers
 | 
						|
        reply_key = nil
 | 
						|
 | 
						|
        Array(message.references).each do |message_id|
 | 
						|
          reply_key = Gitlab::IncomingEmail.key_from_fallback_reply_message_id(message_id)
 | 
						|
          break if reply_key
 | 
						|
        end
 | 
						|
 | 
						|
        reply_key
 | 
						|
      end
 | 
						|
 | 
						|
      def sent_notification
 | 
						|
        return nil unless reply_key
 | 
						|
 | 
						|
        SentNotification.for(reply_key)
 | 
						|
      end
 | 
						|
 | 
						|
      def add_attachments(reply, project)
 | 
						|
        attachments = Email::AttachmentUploader.new(message).execute(project)
 | 
						|
 | 
						|
        attachments.each do |link|
 | 
						|
          reply << "\n\n#{link[:markdown]}"
 | 
						|
        end
 | 
						|
 | 
						|
        reply
 | 
						|
      end
 | 
						|
 | 
						|
      def create_note(reply)
 | 
						|
        Notes::CreateService.new(
 | 
						|
          sent_notification.project,
 | 
						|
          sent_notification.recipient,
 | 
						|
          note:           reply,
 | 
						|
          noteable_type:  sent_notification.noteable_type,
 | 
						|
          noteable_id:    sent_notification.noteable_id,
 | 
						|
          commit_id:      sent_notification.commit_id,
 | 
						|
          line_code:      sent_notification.line_code
 | 
						|
        ).execute
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
end
 |