186 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			Ruby
		
	
	
	
			
		
		
	
	
			186 lines
		
	
	
		
			5.3 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 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
 | |
|           process_create_issue
 | |
| 
 | |
|         else
 | |
|           # TODO: could also be project not found
 | |
|           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
 | |
| 
 | |
|       # Find the first matched user in database from email From: section
 | |
|       # TODO: Since this address could be forged, we should have some kind of
 | |
|       #       auth token attached somewhere to verify the identity better.
 | |
|       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
 |