Files
gitlabhq/lib/gitlab/email_receiver.rb
T
2015-08-18 17:02:26 -07:00

157 lines
4.3 KiB
Ruby

# Inspired in great part by Discourse's Email::Receiver
module Gitlab
class EmailReceiver
class ProcessingError < StandardError; end
class EmailUnparsableError < ProcessingError; end
class EmptyEmailError < ProcessingError; end
class UserNotFoundError < ProcessingError; end
class UserNotAuthorizedLevelError < ProcessingError; end
class NoteableNotFoundError < ProcessingError; end
class AutoGeneratedEmailError < ProcessingError; end
class SentNotificationNotFound < ProcessingError; end
class InvalidNote < ProcessingError; end
def initialize(raw)
@raw = raw
end
def message
@message ||= Mail::Message.new(@raw)
rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError => e
raise EmailUnparsableError, e
end
def process
raise EmptyEmailError if @raw.blank?
raise AutoGeneratedEmailError if message.header.to_s =~ /auto-(generated|replied)/
raise SentNotificationNotFound unless sent_notification
author = sent_notification.recipient
raise UserNotFoundError unless author
project = sent_notification.project
raise UserNotAuthorizedLevelError unless author.can?(:create_note, project)
raise NoteableNotFoundError unless sent_notification.noteable
body = parse_body(message)
note = Notes::CreateService.new(
project,
author,
note: body,
noteable_type: sent_notification.noteable_type,
noteable_id: sent_notification.noteable_id,
commit_id: sent_notification.commit_id
).execute
unless note.persisted?
raise InvalidNote, note.errors.full_messages.join("\n")
end
end
private
def reply_key
reply_key = nil
message.to.each do |address|
reply_key = Gitlab::ReplyByEmail.reply_key_from_address(address)
break if reply_key
end
reply_key
end
def sent_notification
return nil unless reply_key
SentNotification.for(reply_key)
end
def parse_body(message)
body = select_body(message)
encoding = body.encoding
raise EmptyEmailError if body.strip.blank?
body = discourse_email_trimmer(body)
raise EmptyEmailError if body.strip.blank?
body = EmailReplyParser.parse_reply(body)
raise EmptyEmailError if body.strip.blank?
body.force_encoding(encoding).encode("UTF-8")
end
def select_body(message)
html = nil
text = nil
if message.multipart?
html = fix_charset(message.html_part)
text = fix_charset(message.text_part)
elsif message.content_type =~ /text\/html/
html = fix_charset(message)
end
# prefer plain text
return text if text
if html
body = EmailHtmlCleaner.new(html).output_html
else
body = fix_charset(message)
end
# Certain trigger phrases that means we didn't parse correctly
if body =~ /(Content\-Type\:|multipart\/alternative|text\/plain)/
raise EmptyEmailError
end
body
end
# Force encoding to UTF-8 on a Mail::Message or Mail::Part
def fix_charset(object)
return nil if object.nil?
if object.charset
object.body.decoded.force_encoding(object.charset.gsub(/utf8/i, "UTF-8")).encode("UTF-8").to_s
else
object.body.to_s
end
rescue
nil
end
REPLYING_HEADER_LABELS = %w(From Sent To Subject Reply To Cc Bcc Date)
REPLYING_HEADER_REGEX = Regexp.union(REPLYING_HEADER_LABELS.map { |label| "#{label}:" })
def discourse_email_trimmer(body)
lines = body.scrub.lines.to_a
range_end = 0
lines.each_with_index do |l, idx|
break if l =~ /\A\s*\-{3,80}\s*\z/ ||
# This one might be controversial but so many reply lines have years, times and end with a colon.
# Let's try it and see how well it works.
(l =~ /\d{4}/ && l =~ /\d:\d\d/ && l =~ /\:$/) ||
(l =~ /On \w+ \d+,? \d+,?.*wrote:/)
# Headers on subsequent lines
break if (0..2).all? { |off| lines[idx+off] =~ REPLYING_HEADER_REGEX }
# Headers on the same line
break if REPLYING_HEADER_LABELS.count { |label| l.include?(label) } >= 3
range_end = idx
end
lines[0..range_end].join.strip
end
end
end