mirror of
https://github.com/discourse/discourse.git
synced 2024-11-23 19:37:55 +08:00
0d0225133c
A failure condition is eliminated where a topic would be created, but post creation would fail, leaving the forum with a topic without any posts. By asking PostCreator to create the topic instead, inside of its transaction, this failure condition is eliminated. Additionally, attachments are restored to working status. Previously, the attachment code would build up the post raw, but then drop it and not do anything with the result (creating orphaned uploads). By actually placing the raw value back in the options hash, it is included in the created post.
245 lines
7.3 KiB
Ruby
245 lines
7.3 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
|
|
|
|
|
|
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
|
|
|
|
private
|
|
|
|
def create_reply
|
|
create_post_with_attachments(@email_log.user,
|
|
raw: @body,
|
|
topic_id: @email_log.topic_id,
|
|
reply_to_post_number: @email_log.post.post_number)
|
|
end
|
|
|
|
def create_new_topic
|
|
post = create_post_with_attachments(@user,
|
|
raw: @body,
|
|
title: @message.subject,
|
|
category: @category_id)
|
|
|
|
EmailLog.create(
|
|
email_type: "topic_via_incoming_email",
|
|
to_address: @message.from.first, # pick from address because we want the user's email
|
|
topic_id: post.topic.id,
|
|
user_id: @user.id,
|
|
)
|
|
|
|
post
|
|
end
|
|
|
|
def create_post_with_attachments(user, post_opts={})
|
|
options = {
|
|
cooking_options: { traditional_markdown_linebreaks: true },
|
|
}.merge(post_opts)
|
|
|
|
raw = options[:raw]
|
|
|
|
# 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
|
|
|
|
options[:raw] = raw
|
|
|
|
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
|