# # Handles an incoming message # module Email class Receiver class ProcessingError < StandardError; end class EmailUnparsableError < ProcessingError; end class EmptyEmailError < ProcessingError; end class UserNotFoundError < ProcessingError; end class UserNotSufficientTrustLevelError < ProcessingError; end class EmailLogNotFound < ProcessingError; end attr_reader :body, :reply_key, :email_log def initialize(raw) @raw = raw end def is_in_email? @allow_strangers = false if SiteSetting.email_in and SiteSetting.email_in_address == @message.to.first @category_id = SiteSetting.email_in_category.to_i return true end category = Category.find_by_email(@message.to.first) return false if not category @category_id = category.id @allow_strangers = category.email_in_allow_strangers return true end def process raise EmptyEmailError if @raw.blank? @message = Mail::Message.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? if is_in_email? @user = User.find_by_email(@message.from.first) if @user.blank? and @allow_strangers wrap_body_in_quote @user = Discourse.system_user end raise UserNotFoundError if @user.blank? raise UserNotSufficientTrustLevelError.new @user if not @user.has_trust_level?(TrustLevel.levels[SiteSetting.email_in_min_trust.to_i]) create_new_topic else @reply_key = @message.to.first # Extract the `reply_key` from the format the site has specified tokens = SiteSetting.reply_by_email_address.split("%{reply_key}") tokens.each do |t| @reply_key.gsub!(t, "") if t.present? end # Look up the email log for the reply key @email_log = EmailLog.for(reply_key) raise EmailLogNotFound if @email_log.blank? create_reply end end private def wrap_body_in_quote @body = "[quote=\"#{@message.from.first}\"] #{@body} [/quote]" end def parse_body html = nil # If the message is multipart, find the best type for our purposes if @message.multipart? @message.parts.each do |p| if p.content_type =~ /text\/plain/ @body = p.charset ? p.body.decoded.force_encoding(p.charset).encode("UTF-8").to_s : p.body.to_s return @body elsif p.content_type =~ /text\/html/ html = p.charset ? p.body.decoded.force_encoding(p.charset).encode("UTF-8").to_s : p.body.to_s end 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? return 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 create_reply # Try to post the body as a reply creator = PostCreator.new(email_log.user, raw: @body, topic_id: @email_log.topic_id, reply_to_post_number: @email_log.post.post_number, cooking_options: {traditional_markdown_linebreaks: true}) creator.create end def create_new_topic # Try to post the body as a reply topic_creator = TopicCreator.new(@user, Guardian.new(@user), category: @category_id, title: @message.subject) topic = topic_creator.create post_creator = PostCreator.new(@user, raw: @body, topic_id: topic.id, cooking_options: {traditional_markdown_linebreaks: true}) post_creator.create EmailLog.create(email_type: "topic_via_incoming_email", to_address: @message.to.first, topic_id: topic.id, user_id: @user.id) topic end end end