243 lines
7.1 KiB
Ruby
243 lines
7.1 KiB
Ruby
#
|
|
# Handles an incoming message
|
|
#
|
|
|
|
module Email
|
|
|
|
class Receiver
|
|
|
|
include ActionView::Helpers::NumberHelper
|
|
|
|
class ProcessingError < StandardError; end
|
|
class EmailUnparsableError < ProcessingError; end
|
|
class EmptyEmailError < ProcessingError; end
|
|
class UserNotFoundError < ProcessingError; end
|
|
class UserNotSufficientTrustLevelError < ProcessingError; end
|
|
class BadDestinationAddress < ProcessingError; end
|
|
class EmailLogNotFound < ProcessingError; end
|
|
class InvalidPost < ProcessingError; end
|
|
|
|
attr_reader :body, :email_log
|
|
|
|
def initialize(raw)
|
|
@raw = raw
|
|
end
|
|
|
|
def process
|
|
raise EmptyEmailError if @raw.blank?
|
|
|
|
@message = Mail.new(@raw)
|
|
|
|
# First remove the known discourse stuff.
|
|
parse_body
|
|
raise EmptyEmailError if @body.blank?
|
|
|
|
# Then run the github EmailReplyParser on it in case we didn't catch it
|
|
@body = EmailReplyParser.read(@body).visible_text.force_encoding('UTF-8')
|
|
|
|
discourse_email_parser
|
|
raise EmailUnparsableError if @body.blank?
|
|
|
|
dest_info = {type: :invalid, obj: nil}
|
|
@message.to.each do |to_address|
|
|
if dest_info[:type] == :invalid
|
|
dest_info = check_address to_address
|
|
end
|
|
end
|
|
|
|
raise BadDestinationAddress if dest_info[:type] == :invalid
|
|
|
|
if dest_info[:type] == :category
|
|
raise BadDestinationAddress unless SiteSetting.email_in
|
|
category = dest_info[:obj]
|
|
@category_id = category.id
|
|
@allow_strangers = category.email_in_allow_strangers
|
|
|
|
user_email = @message.from.first
|
|
@user = User.find_by_email(user_email)
|
|
if @user.blank? && @allow_strangers
|
|
|
|
wrap_body_in_quote user_email
|
|
# TODO This is WRONG it should register an account
|
|
# and email the user details on how to log in / activate
|
|
@user = Discourse.system_user
|
|
end
|
|
|
|
raise UserNotFoundError if @user.blank?
|
|
raise UserNotSufficientTrustLevelError.new @user unless @allow_strangers || @user.has_trust_level?(TrustLevel.levels[SiteSetting.email_in_min_trust.to_i])
|
|
|
|
create_new_topic
|
|
else
|
|
@email_log = dest_info[:obj]
|
|
|
|
raise EmailLogNotFound if @email_log.blank?
|
|
|
|
create_reply
|
|
end
|
|
end
|
|
|
|
def check_address(address)
|
|
category = Category.find_by_email(address)
|
|
return {type: :category, obj: category} if category
|
|
|
|
regex = Regexp.escape SiteSetting.reply_by_email_address
|
|
regex = regex.gsub(Regexp.escape('%{reply_key}'), "(.*)")
|
|
regex = Regexp.new regex
|
|
match = regex.match address
|
|
if match && match[1].present?
|
|
reply_key = match[1]
|
|
email_log = EmailLog.for(reply_key)
|
|
|
|
return {type: :reply, obj: email_log}
|
|
end
|
|
|
|
{type: :invalid, obj: nil}
|
|
end
|
|
|
|
private
|
|
|
|
def parse_body
|
|
html = nil
|
|
|
|
# If the message is multipart, find the best type for our purposes
|
|
if @message.multipart?
|
|
if p = @message.text_part
|
|
@body = p.charset ? p.body.decoded.force_encoding(p.charset).encode("UTF-8").to_s : p.body.to_s
|
|
return @body
|
|
elsif p = @message.html_part
|
|
html = p.charset ? p.body.decoded.force_encoding(p.charset).encode("UTF-8").to_s : p.body.to_s
|
|
end
|
|
end
|
|
|
|
if @message.content_type =~ /text\/html/
|
|
if defined? @message.charset
|
|
html = @message.body.decoded.force_encoding(@message.charset).encode("UTF-8").to_s
|
|
else
|
|
html = @message.body.to_s
|
|
end
|
|
end
|
|
|
|
if html.present?
|
|
@body = scrub_html(html)
|
|
return @body
|
|
end
|
|
|
|
@body = @message.charset ? @message.body.decoded.force_encoding(@message.charset).encode("UTF-8").to_s.strip : @message.body.to_s
|
|
|
|
# Certain trigger phrases that means we didn't parse correctly
|
|
@body = nil if @body =~ /Content\-Type\:/ ||
|
|
@body =~ /multipart\/alternative/ ||
|
|
@body =~ /text\/plain/
|
|
|
|
@body
|
|
end
|
|
|
|
def scrub_html(html)
|
|
# If we have an HTML message, strip the markup
|
|
doc = Nokogiri::HTML(html)
|
|
|
|
# Blackberry is annoying in that it only provides HTML. We can easily extract it though
|
|
content = doc.at("#BB10_response_div")
|
|
return content.text if content.present?
|
|
|
|
doc.xpath("//text()").text
|
|
end
|
|
|
|
def discourse_email_parser
|
|
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/ ||
|
|
l =~ Regexp.new("\\A\\s*" + I18n.t('user_notifications.previous_discussion') + "\\s*\\Z") ||
|
|
(l =~ /via #{SiteSetting.title}(.*)\:$/) ||
|
|
# 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 =~ /\:$/)
|
|
|
|
range_end = idx
|
|
end
|
|
|
|
@body = lines[0..range_end].join
|
|
@body.strip!
|
|
end
|
|
|
|
def wrap_body_in_quote(user_email)
|
|
@body = "[quote=\"#{user_email}\"]
|
|
#{@body}
|
|
[/quote]"
|
|
end
|
|
|
|
def create_reply
|
|
create_post_with_attachments(email_log.user, @body, @email_log.topic_id, @email_log.post.post_number)
|
|
end
|
|
|
|
def create_new_topic
|
|
topic = TopicCreator.new(
|
|
@user,
|
|
Guardian.new(@user),
|
|
category: @category_id,
|
|
title: @message.subject,
|
|
).create
|
|
|
|
post = create_post_with_attachments(@user, @body, topic.id)
|
|
|
|
EmailLog.create(
|
|
email_type: "topic_via_incoming_email",
|
|
to_address: @message.to.first,
|
|
topic_id: topic.id,
|
|
user_id: @user.id,
|
|
)
|
|
|
|
post
|
|
end
|
|
|
|
def create_post_with_attachments(user, raw, topic_id, reply_to_post_number=nil)
|
|
options = {
|
|
raw: raw,
|
|
topic_id: topic_id,
|
|
cooking_options: { traditional_markdown_linebreaks: true },
|
|
}
|
|
options[:reply_to_post_number] = reply_to_post_number if reply_to_post_number
|
|
|
|
# deal with attachments
|
|
@message.attachments.each do |attachment|
|
|
tmp = Tempfile.new("discourse-email-attachment")
|
|
begin
|
|
# read attachment
|
|
File.open(tmp.path, "w+b") { |f| f.write attachment.body.decoded }
|
|
# create the upload for the user
|
|
upload = Upload.create_for(user.id, tmp, attachment.filename, File.size(tmp))
|
|
if upload && upload.errors.empty?
|
|
# TODO: should use the same code as the client to insert attachments
|
|
raw << "\n#{attachment_markdown(upload)}\n"
|
|
end
|
|
ensure
|
|
tmp.close!
|
|
end
|
|
|
|
end
|
|
|
|
create_post(user, options)
|
|
end
|
|
|
|
def attachment_markdown(upload)
|
|
if FileHelper.is_image?(upload.original_filename)
|
|
"<img src='#{upload.url}' width='#{upload.width}' height='#{upload.height}'>"
|
|
else
|
|
"<a class='attachment' href='#{upload.url}'>#{upload.original_filename}</a> (#{number_to_human_size(upload.filesize)})"
|
|
end
|
|
end
|
|
|
|
def create_post(user, options)
|
|
creator = PostCreator.new(user, options)
|
|
post = creator.create
|
|
if creator.errors.present?
|
|
raise InvalidPost, creator.errors.full_messages.join("\n")
|
|
end
|
|
post
|
|
end
|
|
|
|
end
|
|
end
|