From 0d0225133cf01a1bb676901a40a351c43cf9f117 Mon Sep 17 00:00:00 2001
From: riking
Date: Tue, 26 Aug 2014 17:30:12 -0700
Subject: [PATCH 1/5] FIX: Failed incoming emails could create empty topics
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.
---
lib/email/receiver.rb | 38 ++++++++++++++++++++------------------
1 file changed, 20 insertions(+), 18 deletions(-)
diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb
index 3845a97985f..ff3db75f9c3 100644
--- a/lib/email/receiver.rb
+++ b/lib/email/receiver.rb
@@ -94,7 +94,6 @@ module Email
{type: :invalid, obj: nil}
end
- private
def parse_body
html = nil
@@ -168,37 +167,37 @@ module Email
[/quote]"
end
+ private
+
def create_reply
- create_post_with_attachments(email_log.user, @body, @email_log.topic_id, @email_log.post.post_number)
+ 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
- topic = TopicCreator.new(
- @user,
- Guardian.new(@user),
- category: @category_id,
- title: @message.subject,
- ).create
-
- post = create_post_with_attachments(@user, @body, topic.id)
+ 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.to.first,
- topic_id: topic.id,
+ 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, raw, topic_id, reply_to_post_number=nil)
+ def create_post_with_attachments(user, post_opts={})
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
+ }.merge(post_opts)
+
+ raw = options[:raw]
# deal with attachments
@message.attachments.each do |attachment|
@@ -215,9 +214,10 @@ module Email
ensure
tmp.close!
end
-
end
+ options[:raw] = raw
+
create_post(user, options)
end
@@ -232,9 +232,11 @@ module Email
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
From cb55ef47027e8994cbd093b6938989057f03cc9d Mon Sep 17 00:00:00 2001
From: riking
Date: Tue, 26 Aug 2014 12:31:47 -0700
Subject: [PATCH 2/5] Add Email::HtmlCleaner for email processing
This class is in charge of stripping out most of the crap from the HTML
portion of emails that email clients generate, so that it can be sanely
post-processed for signatures and quoting boundaries.
---
lib/email/html_cleaner.rb | 120 ++++++++++++++++++++++++++++++++++++++
1 file changed, 120 insertions(+)
create mode 100644 lib/email/html_cleaner.rb
diff --git a/lib/email/html_cleaner.rb b/lib/email/html_cleaner.rb
new file mode 100644
index 00000000000..19e4b417327
--- /dev/null
+++ b/lib/email/html_cleaner.rb
@@ -0,0 +1,120 @@
+module Email
+ # HtmlCleaner cleans up the extremely dirty HTML that many email clients
+ # generate by stripping out any excess divs or spans, removing styling in
+ # the process (which also makes the html more suitable to be parsed as
+ # Markdown).
+ class HtmlCleaner
+ # Elements to hoist all children out of
+ HTML_HOIST_ELEMENTS = %w(div span font table tbody th tr td)
+ # Node types to always delete
+ HTML_DELETE_ELEMENT_TYPES = [Nokogiri::XML::Node::DTD_NODE,
+ Nokogiri::XML::Node::COMMENT_NODE,
+ ]
+
+ # Private variables:
+ # @doc - nokogiri document
+ # @out - same as @doc, but only if trimming has occured
+ def initialize(html)
+ if String === html
+ @doc = Nokogiri::HTML(html)
+ else
+ @doc = html
+ end
+ end
+
+ class << self
+ # Email::HtmlCleaner.trim(inp, opts={})
+ #
+ # Arguments:
+ # inp - Either a HTML string or a Nokogiri document.
+ # Options:
+ # :return => :doc, :string
+ # Specify the desired return type.
+ # Defaults to the type of the input.
+ # A value of :string is equivalent to calling get_document_text()
+ # on the returned document.
+ def trim(inp, opts={})
+ cleaner = HtmlCleaner.new(inp)
+
+ opts[:return] ||= ((String === inp) ? :string : :doc)
+
+ if opts[:return] == :string
+ cleaner.output_html
+ else
+ cleaner.output_document
+ end
+ end
+
+ # Email::HtmlCleaner.get_document_text(doc)
+ #
+ # Get the body portion of the document, including html, as a string.
+ def get_document_text(doc)
+ body = doc.xpath('//body')
+ if body
+ body.inner_html
+ else
+ doc.inner_html
+ end
+ end
+ end
+
+ def output_document
+ @out ||= begin
+ doc = @doc
+ trim_process_node doc
+ add_newlines doc
+ doc
+ end
+ end
+
+ def output_html
+ HtmlCleaner.get_document_text(output_document)
+ end
+
+ private
+
+ def add_newlines(doc)
+ doc.xpath('//br').each do |br|
+ br.replace(Nokogiri::XML::Text.new("\n", doc))
+ end
+ end
+
+ def trim_process_node(node)
+ if should_hoist?(node)
+ hoisted = trim_hoist_element node
+ hoisted.each { |child| trim_process_node child }
+ elsif should_delete?(node)
+ node.remove
+ else
+ if children = node.children
+ children.each { |child| trim_process_node child }
+ end
+ end
+
+ node
+ end
+
+ def trim_hoist_element(element)
+ hoisted = []
+ element.children.each do |child|
+ element.before(child)
+ hoisted << child
+ end
+ element.remove
+ hoisted
+ end
+
+ def should_hoist?(node)
+ return false unless node.element?
+ HTML_HOIST_ELEMENTS.include? node.name
+ end
+
+ def should_delete?(node)
+ return true if HTML_DELETE_ELEMENT_TYPES.include? node.type
+ return true if node.element? && node.name == 'head'
+ return true if node.text? && node.text.strip.blank?
+
+ false
+ end
+ end
+end
From 0a09593f3b6193010c0d19a20df8dd7d0d56cb52 Mon Sep 17 00:00:00 2001
From: riking
Date: Tue, 26 Aug 2014 17:31:51 -0700
Subject: [PATCH 3/5] FIX: Prefer HTML in incoming emails, heavily refactor
email receiver
This commit heavily refactors Email::Receiver to both better handle
different emails and improve testability.
A primary focus of the refactor is reducing the usage of class
variables, in favor of actually passing parameters - making it possible
for multiple tests to use the same Receiver instance.
The EmailLog reported when a topic is created is reflected to put the
user's email in the to_address field, instead of the system address.
The discourse_email_parser function is renamed to
discourse_email_trimmer, and additional stopping conditions are added to
make up for EmailReplyParser's inability to deal with html at the start
of a line.
The force_encoding calls are refactored out to a 'fix_charset' method.
parse_body is renamed to select_body, and the scrub_html method is
dropped in favor of the new HtmlCleaner class.
A new parse_body method is added, which performs the job of the removed
lines of code in the 'process' method.
EmailUnparsableError is redefined again, to be encoding errors (when the
declared encoding is not what was delivered).
---
lib/email/receiver.rb | 108 +++++++++++++++++++++++-------------------
1 file changed, 59 insertions(+), 49 deletions(-)
diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb
index ff3db75f9c3..c644950cee4 100644
--- a/lib/email/receiver.rb
+++ b/lib/email/receiver.rb
@@ -1,3 +1,4 @@
+require 'email/html_cleaner'
#
# Handles an incoming message
#
@@ -26,20 +27,12 @@ module Email
def process
raise EmptyEmailError if @raw.blank?
- @message = Mail.new(@raw)
+ 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?
+ body = parse_body message
dest_info = {type: :invalid, obj: nil}
- @message.to.each do |to_address|
+ message.to.each do |to_address|
if dest_info[:type] == :invalid
dest_info = check_address to_address
end
@@ -47,6 +40,10 @@ module Email
raise BadDestinationAddress if dest_info[:type] == :invalid
+ # TODO get to a state where we can remove this
+ @message = message
+ @body = body
+
if dest_info[:type] == :category
raise BadDestinationAddress unless SiteSetting.email_in
category = dest_info[:obj]
@@ -74,6 +71,8 @@ module Email
create_reply
end
+ rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError => e
+ raise EmailUnparsableError.new(e)
end
def check_address(address)
@@ -94,56 +93,63 @@ module Email
{type: :invalid, obj: nil}
end
+ def parse_body(message)
+ body = select_body message
+ raise EmptyEmailError if body.strip.blank?
- def parse_body
+ body = discourse_email_trimmer body
+ raise EmptyEmailError if body.strip.blank?
+
+ body = EmailReplyParser.parse_reply body
+ raise EmptyEmailError if body.strip.blank?
+
+ body
+ end
+
+ def select_body(message)
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
+ # If the message is multipart, return that part (favor html)
+ if message.multipart?
+ html = fix_charset message.html_part
+ text = fix_charset message.text_part
+ # TODO picking text if available may be better
+ if text && !html
+ return text
end
+ elsif message.content_type =~ /text\/html/
+ html = fix_charset message
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
+ if html
+ body = HtmlCleaner.new(html).output_html
+ else
+ body = fix_charset message
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/
+ if body =~ /Content\-Type\:/ || body =~ /multipart\/alternative/ || body =~ /text\/plain/
+ raise EmptyEmailError
+ end
- @body
+ body
end
- def scrub_html(html)
- # If we have an HTML message, strip the markup
- doc = Nokogiri::HTML(html)
+ # Force encoding to UTF-8 on a Mail::Message or Mail::Part
+ def fix_charset(object)
+ return nil if object.nil?
- # 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
+ if object.charset
+ object.body.decoded.force_encoding(object.charset).encode("UTF-8").to_s
+ else
+ object.body.to_s
+ end
end
- def discourse_email_parser
- lines = @body.scrub.lines.to_a
+ REPLYING_HEADER_LABELS = ['From', 'Sent', 'To', 'Subject', 'Reply To']
+ REPLYING_HEADER_REGEX = Regexp.union(REPLYING_HEADER_LABELS.map { |lbl| "#{lbl}:" })
+
+ def discourse_email_trimmer(body)
+ lines = body.scrub.lines.to_a
range_end = 0
lines.each_with_index do |l, idx|
@@ -154,11 +160,15 @@ module Email
# Let's try it and see how well it works.
(l =~ /\d{4}/ && l =~ /\d:\d\d/ && l =~ /\:$/)
+ # 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 { |lbl| l.include? lbl } >= 3
+
range_end = idx
end
- @body = lines[0..range_end].join
- @body.strip!
+ lines[0..range_end].join.strip
end
def wrap_body_in_quote(user_email)
From 1c9f6159cd6a03a4dc74b0912f475fc73c39d613 Mon Sep 17 00:00:00 2001
From: riking
Date: Tue, 26 Aug 2014 17:08:53 -0700
Subject: [PATCH 4/5] Update the Receiver and PollMailbox specs for the changes
Tests are both added, moved, and deleted.
Add test for topic not being created
Move html_only.eml to parse_body testing section
---
spec/components/email/receiver_spec.rb | 271 ++++++++++++++-------
spec/fixtures/emails/boundary.eml | 4 +-
spec/fixtures/emails/multiline_wrote.eml | 23 --
spec/fixtures/emails/multipart.eml | 67 -----
spec/fixtures/emails/paragraphs.cooked | 7 +
spec/fixtures/emails/too_many_mentions.eml | 31 +++
spec/fixtures/emails/too_short.eml | 21 ++
spec/jobs/poll_mailbox_spec.rb | 4 +-
spec/spec_helper.rb | 1 +
9 files changed, 247 insertions(+), 182 deletions(-)
delete mode 100644 spec/fixtures/emails/multiline_wrote.eml
delete mode 100644 spec/fixtures/emails/multipart.eml
create mode 100644 spec/fixtures/emails/paragraphs.cooked
create mode 100644 spec/fixtures/emails/too_many_mentions.eml
create mode 100644 spec/fixtures/emails/too_short.eml
diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb
index 71bc6a4d504..c5ef25543ec 100644
--- a/spec/components/email/receiver_spec.rb
+++ b/spec/components/email/receiver_spec.rb
@@ -8,124 +8,225 @@ describe Email::Receiver do
before do
SiteSetting.reply_by_email_address = "reply+%{reply_key}@appmail.adventuretime.ooo"
SiteSetting.email_in = false
+ SiteSetting.title = "Discourse"
end
- describe 'invalid emails' do
+ describe 'parse_body' do
+ def test_parse_body(mail_string)
+ Email::Receiver.new(nil).parse_body(Mail::Message.new mail_string)
+ end
+
it "raises EmptyEmailError if the message is blank" do
- expect { Email::Receiver.new("").process }.to raise_error(Email::Receiver::EmptyEmailError)
+ expect { test_parse_body("") }.to raise_error(Email::Receiver::EmptyEmailError)
end
it "raises EmptyEmailError if the message is not an email" do
- expect { Email::Receiver.new("asdf" * 30).process}.to raise_error(Email::Receiver::EmptyEmailError)
+ expect { test_parse_body("asdf" * 30) }.to raise_error(Email::Receiver::EmptyEmailError)
end
- it "raises EmailUnparsableError if there is no reply content" do
- expect { Email::Receiver.new(fixture_file("emails/no_content_reply.eml")).process}.to raise_error(Email::Receiver::EmailUnparsableError)
+ it "raises EmptyEmailError if there is no reply content" do
+ expect { test_parse_body(fixture_file("emails/no_content_reply.eml")) }.to raise_error(Email::Receiver::EmptyEmailError)
end
- end
- describe "with multipart" do
- let(:reply_below) { fixture_file("emails/multipart.eml") }
- let(:receiver) { Email::Receiver.new(reply_below) }
-
- it "processes correctly" do
- expect { receiver.process}.to raise_error(Email::Receiver::EmailLogNotFound)
- expect(receiver.body).to eq(
-"So presumably all the quoted garbage and my (proper) signature will get
-stripped from my reply?")
+ pending "raises EmailUnparsableError if the headers are corrupted" do
+ expect { ; }.to raise_error(Email::Receiver::EmailUnparsableError)
end
- end
- describe "html only" do
- let(:reply_below) { fixture_file("emails/html_only.eml") }
- let(:receiver) { Email::Receiver.new(reply_below) }
-
- it "processes correctly" do
- expect { receiver.process}.to raise_error(Email::Receiver::EmailLogNotFound)
- expect(receiver.body).to eq("The EC2 instance - I've seen that there tends to be odd and " +
- "unrecommended settings on the Bitnami installs that I've checked out.")
+ it "can parse the html section" do
+ test_parse_body(fixture_file("emails/html_only.eml")).should == "The EC2 instance - I've seen that there tends to be odd and " +
+ "unrecommended settings on the Bitnami installs that I've checked out."
end
- end
- describe "it supports a dutch reply" do
- let(:dutch) { fixture_file("emails/dutch.eml") }
- let(:receiver) { Email::Receiver.new(dutch) }
-
- it "processes correctly" do
- expect { receiver.process}.to raise_error(Email::Receiver::EmailLogNotFound)
- expect(receiver.body).to eq("Dit is een antwoord in het Nederlands.")
+ it "supports a Dutch reply" do
+ test_parse_body(fixture_file("emails/dutch.eml")).should == "Dit is een antwoord in het Nederlands."
end
- end
- describe "It supports a non english reply" do
- let(:hebrew) { fixture_file("emails/hebrew.eml") }
- let(:receiver) { Email::Receiver.new(hebrew) }
-
- it "processes correctly" do
+ it "supports a Hebrew reply" do
I18n.expects(:t).with('user_notifications.previous_discussion').returns('כלטוב')
- expect { receiver.process}.to raise_error(Email::Receiver::EmailLogNotFound)
- expect(receiver.body).to eq("שלום")
+
+ # The force_encoding call is only needed for the test - it is passed on fine to the cooked post
+ test_parse_body(fixture_file("emails/hebrew.eml")).force_encoding("UTF-8").should == "שלום"
end
- end
- describe "It supports a non UTF-8 reply" do
- let(:big5) { fixture_file("emails/big5.eml") }
- let(:receiver) { Email::Receiver.new(big5) }
-
- it "processes correctly" do
+ it "supports a BIG5-encoded reply" do
I18n.expects(:t).with('user_notifications.previous_discussion').returns('媽!我上電視了!')
- expect { receiver.process}.to raise_error(Email::Receiver::EmailLogNotFound)
- expect(receiver.body).to eq("媽!我上電視了!")
+
+ # The force_encoding call is only needed for the test - it is passed on fine to the cooked post
+ test_parse_body(fixture_file("emails/big5.eml")).force_encoding("UTF-8").should == "媽!我上電視了!"
end
- end
- describe "via" do
- let(:wrote) { fixture_file("emails/via_line.eml") }
- let(:receiver) { Email::Receiver.new(wrote) }
+ it "removes 'via' lines if they match the site title" do
+ SiteSetting.title = "Discourse"
- it "removes via lines if we know them" do
- expect { receiver.process}.to raise_error(Email::Receiver::EmailLogNotFound)
- expect(receiver.body).to eq("Hello this email has content!")
+ test_parse_body(fixture_file("emails/via_line.eml")).should == "Hello this email has content!"
end
- end
- describe "if wrote is on a second line" do
- let(:wrote) { fixture_file("emails/multiline_wrote.eml") }
- let(:receiver) { Email::Receiver.new(wrote) }
-
- it "processes correctly" do
- expect { receiver.process}.to raise_error(Email::Receiver::EmailLogNotFound)
- expect(receiver.body).to eq("Thanks!")
+ it "removes the 'Previous Discussion' marker" do
+ test_parse_body(fixture_file("emails/previous.eml")).should == "This will not include the previous discussion that is present in this email."
end
- end
- describe "remove previous discussion" do
- let(:previous) { fixture_file("emails/previous.eml") }
- let(:receiver) { Email::Receiver.new(previous) }
-
- it "processes correctly" do
- expect { receiver.process}.to raise_error(Email::Receiver::EmailLogNotFound)
- expect(receiver.body).to eq("This will not include the previous discussion that is present in this email.")
- end
- end
-
- describe "multiple paragraphs" do
- let(:paragraphs) { fixture_file("emails/paragraphs.eml") }
- let(:receiver) { Email::Receiver.new(paragraphs) }
-
- it "processes correctly" do
- expect { receiver.process}.to raise_error(Email::Receiver::EmailLogNotFound)
- expect(receiver.body).to eq(
+ it "handles multiple paragraphs" do
+ test_parse_body(fixture_file("emails/paragraphs.eml")).
+ should == (
"Is there any reason the *old* candy can't be be kept in silos while the new candy
is imported into *new* silos?
The thing about candy is it stays delicious for a long time -- we can just keep
it there without worrying about it too much, imo.
-Thanks for listening.")
+Thanks for listening."
+ )
end
end
+ describe "posting replies" do
+ let(:reply_key) { raise "Override this in a lower describe block" }
+ let(:email_raw) { raise "Override this in a lower describe block" }
+ # ----
+ let(:receiver) { Email::Receiver.new(email_raw) }
+ let(:post) { create_post }
+ let(:topic) { post.topic }
+ let(:posting_user) { post.user }
+ let(:replying_user_email) { 'jake@adventuretime.ooo' }
+ let(:replying_user) { Fabricate(:user, email: replying_user_email, trust_level: 2)}
+ let(:email_log) { EmailLog.new(reply_key: reply_key,
+ post: post,
+ post_id: post.id,
+ topic_id: post.topic_id,
+ email_type: 'user_posted',
+ user: replying_user,
+ user_id: replying_user.id,
+ to_address: replying_user_email
+ ) }
+
+ before do
+ email_log.save
+ end
+
+ # === Success Posting ===
+
+ describe "valid_reply.eml" do
+ let!(:reply_key) { '59d8df8370b7e95c5a49fbf86aeb2c93' }
+ let!(:email_raw) { fixture_file("emails/valid_reply.eml") }
+
+ it "creates a post with the correct content" do
+ start_count = topic.posts.count
+
+ receiver.process
+
+ topic.posts.count.should == (start_count + 1)
+ topic.posts.last.cooked.strip.should == fixture_file("emails/valid_reply.cooked").strip
+ end
+ end
+
+ describe "paragraphs.eml" do
+ let!(:reply_key) { '59d8df8370b7e95c5a49fbf86aeb2c93' }
+ let!(:email_raw) { fixture_file("emails/paragraphs.eml") }
+
+ it "cooks multiple paragraphs with traditional Markdown linebreaks" do
+ start_count = topic.posts.count
+
+ receiver.process
+
+ topic.posts.count.should == (start_count + 1)
+ topic.posts.last.cooked.strip.should == fixture_file("emails/paragraphs.cooked").strip
+ topic.posts.last.cooked.should_not match /
/
+ Upload.find_by(sha1: upload_sha).should_not be_nil
+ end
+
+ end
+
+ # === Failure Conditions ===
+
+ describe "too_short.eml" do
+ let!(:reply_key) { '636ca428858779856c226bb145ef4fad' }
+ let!(:email_raw) {
+ fixture_file("emails/too_short.eml")
+ .gsub("TO", "reply+#{reply_key}@appmail.adventuretime.ooo")
+ .gsub("FROM", replying_user_email)
+ .gsub("SUBJECT", "re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux'")
+ }
+
+ it "raises an InvalidPost error" do
+ SiteSetting.min_post_length = 5
+ expect { receiver.process }.to raise_error(Email::Receiver::InvalidPost)
+ end
+ end
+
+ describe "too_many_mentions.eml" do
+ let!(:reply_key) { '636ca428858779856c226bb145ef4fad' }
+ let!(:email_raw) { fixture_file("emails/too_many_mentions.eml") }
+
+ it "raises an InvalidPost error" do
+ SiteSetting.max_mentions_per_post = 10
+ (1..11).each do |i|
+ Fabricate(:user, username: "user#{i}").save
+ end
+
+ expect { receiver.process }.to raise_error(Email::Receiver::InvalidPost)
+ end
+ end
+
+ end
+
+ describe "posting a new topic" do
+ let(:category_destination) { raise "Override this in a lower describe block" }
+ let(:email_raw) { raise "Override this in a lower describe block" }
+ let(:allow_strangers) { false }
+ # ----
+ let(:receiver) { Email::Receiver.new(email_raw) }
+ let(:user_email) { 'jake@adventuretime.ooo' }
+ let(:user) { Fabricate(:user, email: user_email, trust_level: 2)}
+ let(:category) { Fabricate(:category, email_in: category_destination, email_in_allow_strangers: allow_strangers) }
+
+ before do
+ SiteSetting.email_in = true
+ user.save
+ category.save
+ end
+
+ describe "too_short.eml" do
+ let!(:category_destination) { 'incoming+amazing@appmail.adventuretime.ooo' }
+ let(:email_raw) {
+ fixture_file("emails/too_short.eml")
+ .gsub("TO", category_destination)
+ .gsub("FROM", user_email)
+ .gsub("SUBJECT", "A long subject that passes the checks")
+ }
+
+ it "does not create a topic if the post fails" do
+ before_topic_count = Topic.count
+
+ expect { receiver.process }.to raise_error(Email::Receiver::InvalidPost)
+
+ Topic.count.should == before_topic_count
+ end
+
+ end
+
+ end
+
def fill_email(mail, from, to, body = nil, subject = nil)
result = mail.gsub("FROM", from).gsub("TO", to)
if body
@@ -181,12 +282,6 @@ greatest show ever created. Everyone should watch it.
expect(receiver.body).to eq(reply_body)
expect(receiver.email_log).to eq(email_log)
-
- attachment_email = fixture_file("emails/attachment.eml")
- attachment_email = fill_email(attachment_email, "test@test.com", to)
- r = Email::Receiver.new(attachment_email)
- expect { r.process }.to_not raise_error
- expect(r.body).to match(/here is an image attachment\n\n/)
end
end
diff --git a/spec/fixtures/emails/boundary.eml b/spec/fixtures/emails/boundary.eml
index 92eb4347f9c..1250fe498b0 100644
--- a/spec/fixtures/emails/boundary.eml
+++ b/spec/fixtures/emails/boundary.eml
@@ -18,7 +18,7 @@ Content-Type: text/plain; charset=ISO-8859-1
I'll look into it, thanks!
-On Wednesday, June 19, 2013, jake via Adventure Time wrote:
+On Wednesday, June 19, 2013, jake via Discourse wrote:
> jake mentioned you in 'peppermint butler is missing' on Adventure
> Time:
@@ -58,4 +58,4 @@ p>
ime.ooo/user_preferences" target=3D"_blank">user preferences.
---001a11c206a073876a04df81d2a9--
\ No newline at end of file
+--001a11c206a073876a04df81d2a9--
diff --git a/spec/fixtures/emails/multiline_wrote.eml b/spec/fixtures/emails/multiline_wrote.eml
deleted file mode 100644
index 0829990dca5..00000000000
--- a/spec/fixtures/emails/multiline_wrote.eml
+++ /dev/null
@@ -1,23 +0,0 @@
-
-Delivered-To: discourse-reply+cd480e301683c9902891f15968bf07a5@discourse.org
-Received: by 10.194.216.104 with SMTP id op8csp80593wjc;
- Wed, 24 Jul 2013 07:59:14 -0700 (PDT)
-Return-Path:
-References: <51efeb9b36c34_66dc2dfce6811866@discourse.mail>
-From: Walter White
-In-Reply-To: <51efeb9b36c34_66dc2dfce6811866@discourse.mail>
-Mime-Version: 1.0 (1.0)
-Date: Wed, 24 Jul 2013 15:59:10 +0100
-Message-ID: <4597127794206131679@unknownmsgid>
-Subject: Re: [Discourse] new reply to your post in 'Crystal Blue'
-To: walter via Discourse
-Content-Type: multipart/alternative; boundary=001a11c20edc15a39304e2432790
-
-Thanks!
-
-On 24 Jul 2013, at 15:58, walter via Discourse
-wrote:
-
- walter July 24
-
-You look great today Walter.
diff --git a/spec/fixtures/emails/multipart.eml b/spec/fixtures/emails/multipart.eml
deleted file mode 100644
index b61f9fa4848..00000000000
--- a/spec/fixtures/emails/multipart.eml
+++ /dev/null
@@ -1,67 +0,0 @@
-Message-ID: <51C22E52.1030509@darthvader.ca>
-Date: Wed, 19 Jun 2013 18:18:58 -0400
-From: Anakin Skywalker
-User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:17.0) Gecko/20130510 Thunderbird/17.0.6
-MIME-Version: 1.0
-To: Han Solo via Death Star
-Subject: Re: [Death Star] [PM] re: Regarding your post in "Site Customization
- not working"
-References: <51d23d33f41fb_5f4e4b35d7d60798@xwing.mail>
-In-Reply-To: <51d23d33f41fb_5f4e4b35d7d60798@xwing.mail>
-Content-Type: multipart/alternative;
- boundary="------------070503080300090900010604"
-
-This is a multi-part message in MIME format.
---------------070503080300090900010604
-Content-Type: text/plain; charset=UTF-8
-Content-Transfer-Encoding: 7bit
-
-So presumably all the quoted garbage and my (proper) signature will get
-stripped from my reply?
-
---
-Anakin Skywalker | `One of the main causes of the fall of
-evildad@darthvader.ca | the Roman Empire was that, lacking zero,
- | they had no way to indicate successful
- | termination of their C programs.' - Firth
-
-
---------------070503080300090900010604
-Content-Type: text/html; charset=UTF-8
-Content-Transfer-Encoding: 7bit
-
-
-
-
-
-
- On 13-06-19 06:14 PM, Han Solo via
- Death Star wrote:
-
-
- Han Solo just sent you a private message
-
- I got it here! Yay it worked!
-
- Please visit this link to respond: http://darthvader.ca/t/regarding-your-post-in-site-customization-not-working/7641/2
- To unsubscribe from these emails, visit your user
- preferences.
-
- So presumably all the quoted garbage and my (proper) signature will
- get stripped from my reply?
-
- --
-Anakin Skywalker | `One of the main causes of the fall of
-evildad@darthvader.ca | the Roman Empire was that, lacking zero,
- | they had no way to indicate successful
- | termination of their C programs.' - Firth
-
-
-
-
---------------070503080300090900010604--
diff --git a/spec/fixtures/emails/paragraphs.cooked b/spec/fixtures/emails/paragraphs.cooked
new file mode 100644
index 00000000000..da83260e09c
--- /dev/null
+++ b/spec/fixtures/emails/paragraphs.cooked
@@ -0,0 +1,7 @@
+Is there any reason the old candy can't be be kept in silos while the new candy
+is imported into new silos?
+
+The thing about candy is it stays delicious for a long time -- we can just keep
+it there without worrying about it too much, imo.
+
+Thanks for listening.
\ No newline at end of file
diff --git a/spec/fixtures/emails/too_many_mentions.eml b/spec/fixtures/emails/too_many_mentions.eml
new file mode 100644
index 00000000000..9cc7b75c94f
--- /dev/null
+++ b/spec/fixtures/emails/too_many_mentions.eml
@@ -0,0 +1,31 @@
+Return-Path:
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for ; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for ; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog
+To: reply+636ca428858779856c226bb145ef4fad@appmail.adventuretime.ooo
+Message-ID:
+Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux'
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
+
+
+@user1
+@user2
+@user3
+@user4
+@user5
+@user6
+@user7
+@user8
+@user9
+@user10
+@user11
\ No newline at end of file
diff --git a/spec/fixtures/emails/too_short.eml b/spec/fixtures/emails/too_short.eml
new file mode 100644
index 00000000000..54fed0f98c5
--- /dev/null
+++ b/spec/fixtures/emails/too_short.eml
@@ -0,0 +1,21 @@
+Return-Path:
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for ; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for ; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog
+To: TO
+Message-ID:
+Subject: SUBJECT
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
+
+
++1
\ No newline at end of file
diff --git a/spec/jobs/poll_mailbox_spec.rb b/spec/jobs/poll_mailbox_spec.rb
index b10dcc09ede..3084b2b2fee 100644
--- a/spec/jobs/poll_mailbox_spec.rb
+++ b/spec/jobs/poll_mailbox_spec.rb
@@ -202,9 +202,9 @@ describe Jobs::PollMailbox do
email.should be_deleted
end
- it "a no content reply raises an EmailUnparsableError" do
+ it "a no content reply raises an EmptyEmailError" do
email = MockPop3EmailObject.new fixture_file('emails/no_content_reply.eml')
- expect_exception Email::Receiver::EmailUnparsableError
+ expect_exception Email::Receiver::EmptyEmailError
poller.handle_mail(email)
email.should be_deleted
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 23c827e7db4..f90de100e8b 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -30,6 +30,7 @@ Spork.prefork do
# Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories.
Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}
+ Dir[Rails.root.join("spec/fabricators/*.rb")].each {|f| require f}
# let's not run seed_fu every test
SeedFu.quiet = true if SeedFu.respond_to? :quiet
From 8ddd90daa42f7487b81b1d6bbd2d68b15b7fa42b Mon Sep 17 00:00:00 2001
From: riking
Date: Thu, 28 Aug 2014 12:09:42 -0700
Subject: [PATCH 5/5] Have parse_body() recover from ASCII-8BIT encoding
Added a test to make sure that the result can be passed into TextCleaner
(which expects UTF-8)
---
lib/email/receiver.rb | 3 ++-
spec/components/email/receiver_spec.rb | 14 ++++++++++++--
2 files changed, 14 insertions(+), 3 deletions(-)
diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb
index c644950cee4..c15f8c42739 100644
--- a/lib/email/receiver.rb
+++ b/lib/email/receiver.rb
@@ -95,6 +95,7 @@ module Email
def parse_body(message)
body = select_body message
+ encoding = body.encoding
raise EmptyEmailError if body.strip.blank?
body = discourse_email_trimmer body
@@ -103,7 +104,7 @@ module Email
body = EmailReplyParser.parse_reply body
raise EmptyEmailError if body.strip.blank?
- body
+ body.force_encoding(encoding).encode("UTF-8")
end
def select_body(message)
diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb
index c5ef25543ec..a0431606773 100644
--- a/spec/components/email/receiver_spec.rb
+++ b/spec/components/email/receiver_spec.rb
@@ -45,14 +45,14 @@ describe Email::Receiver do
I18n.expects(:t).with('user_notifications.previous_discussion').returns('כלטוב')
# The force_encoding call is only needed for the test - it is passed on fine to the cooked post
- test_parse_body(fixture_file("emails/hebrew.eml")).force_encoding("UTF-8").should == "שלום"
+ test_parse_body(fixture_file("emails/hebrew.eml")).should == "שלום"
end
it "supports a BIG5-encoded reply" do
I18n.expects(:t).with('user_notifications.previous_discussion').returns('媽!我上電視了!')
# The force_encoding call is only needed for the test - it is passed on fine to the cooked post
- test_parse_body(fixture_file("emails/big5.eml")).force_encoding("UTF-8").should == "媽!我上電視了!"
+ test_parse_body(fixture_file("emails/big5.eml")).should == "媽!我上電視了!"
end
it "removes 'via' lines if they match the site title" do
@@ -77,6 +77,16 @@ it there without worrying about it too much, imo.
Thanks for listening."
)
end
+
+ it "converts back to UTF-8 at the end" do
+ result = test_parse_body(fixture_file("emails/big5.eml"))
+ result.encoding.should == Encoding::UTF_8
+
+ # should not throw
+ TextCleaner.normalize_whitespaces(
+ test_parse_body(fixture_file("emails/big5.eml"))
+ )
+ end
end
describe "posting replies" do