gitlab-ce/lib/gitlab/email/receiver.rb

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