# # 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) "" else "#{upload.original_filename} (#{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