2
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2025-09-06 10:50:21 +08:00

Switch to proper exception handling system for better user feedback

- Replace implicit return code-system in Email::Receiver with proper exception system
 - Update tests to check for exceptions instead
 - Test the PollMailbox for expected failures
 - Add proper email-handling of problematic emails
"
This commit is contained in:
Benjamin Kampmann 2014-02-28 13:05:09 +01:00
parent d32cb55837
commit 024597e643
6 changed files with 198 additions and 110 deletions

View file

@ -18,6 +18,26 @@ module Jobs
end end
end end
def handle_mail(mail)
begin
Email::Receiver.new(mail).process
rescue Email::Receiver::UserNotSufficientTrustLevelError => e
# inform the user about the rejection
@message = Mail::Message.new(mail)
clientMessage = RejectionMailer.send_trust_level(@message.from, @message.body)
email_sender = Email::Sender.new(clientMessage, :email_reject_trust_level)
email_sender.send
rescue Email::Receiver::ProcessingError
# all other ProcessingErrors are ok to be dropped
rescue StandardError => e
# Inform Admins about error
GroupMessage.create(Group[:admins].name, :email_error_notification,
{limit_once_per: false, message_params: {source: mail, error: e}})
ensure
mail.delete
end
end
def poll_pop3s def poll_pop3s
Net::POP3.enable_ssl(OpenSSL::SSL::VERIFY_NONE) Net::POP3.enable_ssl(OpenSSL::SSL::VERIFY_NONE)
Net::POP3.start(SiteSetting.pop3s_polling_host, Net::POP3.start(SiteSetting.pop3s_polling_host,
@ -26,15 +46,7 @@ module Jobs
SiteSetting.pop3s_polling_password) do |pop| SiteSetting.pop3s_polling_password) do |pop|
unless pop.mails.empty? unless pop.mails.empty?
pop.each do |mail| pop.each do |mail|
if Email::Receiver.new(mail.pop).process == Email::Receiver.results[:processed] handle_mail mail.pop
mail.delete
else
@message = Mail::Message.new(mail.pop)
# One for you (mod), and one for me (sender)
GroupMessage.create(Group[:moderators].name, :email_reject_notification, {limit_once_per: false, message_params: {from: @message.from, body: @message.body}})
clientMessage = RejectionMailer.send_rejection(@message.from, @message.body)
Email::Sender.new(clientMessage, :email_reject_notification).send
end
end end
end end
end end

View file

@ -6,4 +6,8 @@ class RejectionMailer < ActionMailer::Base
def send_rejection(from, body) def send_rejection(from, body)
build_email(from, template: 'email_reject_notification', from: from, body: body) build_email(from, template: 'email_reject_notification', from: from, body: body)
end end
def send_trust_level(from, body, to)
build_email(from, template: 'email_reject_trust_level', to: to)
end
end end

View file

@ -1111,17 +1111,23 @@ en:
subject_template: "Import completed successfully" subject_template: "Import completed successfully"
text_body_template: "The import was successful." text_body_template: "The import was successful."
email_reject_notification: email_error_notification:
subject_template: "Message posting failed" subject_template: "Error parsing email"
text_body_template: | text_body_template: |
This is an automated message to inform you that the user a message failed to meet topic criteria. This is an automated message to inform you that parsing the following incoming email failed.
Please review the following message. Please review the following message.
From - %{from} Error - %{error}
Contents - %{body}. %{source}
email_reject_trust_level:
subject_template: "Message rejected"
text_body_template: |
The message you've send to %{to} was rejected by the system.
You do not have the required trust to post new topics to this email address.
too_many_spam_flags: too_many_spam_flags:
subject_template: "New account blocked" subject_template: "New account blocked"

View file

@ -5,9 +5,12 @@
module Email module Email
class Receiver class Receiver
def self.results class ProcessingError < StandardError; end
@results ||= Enum.new(:unprocessable, :missing, :processed, :error) class EmailUnparsableError < ProcessingError; end
end class EmptyEmailError < ProcessingError; end
class UserNotFoundError < ProcessingError; end
class UserNotSufficientTrustLevelError < ProcessingError; end
class EmailLogNotFound < ProcessingError; end
attr_reader :body, :reply_key, :email_log attr_reader :body, :reply_key, :email_log
@ -32,21 +35,21 @@ module Email
end end
def process def process
return Email::Receiver.results[:unprocessable] if @raw.blank? raise EmptyEmailError if @raw.blank?
@message = Mail::Message.new(@raw) @message = Mail::Message.new(@raw)
# First remove the known discourse stuff. # First remove the known discourse stuff.
parse_body parse_body
return Email::Receiver.results[:unprocessable] if @body.blank? raise EmptyEmailError if @body.blank?
# Then run the github EmailReplyParser on it in case we didn't catch it # Then run the github EmailReplyParser on it in case we didn't catch it
@body = EmailReplyParser.read(@body).visible_text.force_encoding('UTF-8') @body = EmailReplyParser.read(@body).visible_text.force_encoding('UTF-8')
discourse_email_parser discourse_email_parser
return Email::Receiver.results[:unprocessable] if @body.blank? raise EmailUnparsableError if @body.blank?
if is_in_email? if is_in_email?
@user = User.find_by_email(@message.from.first) @user = User.find_by_email(@message.from.first)
@ -55,29 +58,25 @@ module Email
@user = Discourse.system_user @user = Discourse.system_user
end end
return Email::Receiver.results[:unprocessable] if @user.blank? or not @user.has_trust_level?(TrustLevel.levels[SiteSetting.email_in_min_trust.to_i]) 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 create_new_topic
return Email::Receiver.results[:processed] 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
@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)
return Email::Receiver.results[:missing] if @email_log.blank?
create_reply
Email::Receiver.results[:processed]
rescue
Email::Receiver.results[:error]
end end
private private

View file

@ -0,0 +1,107 @@
# -*- encoding : utf-8 -*-
require 'spec_helper'
require 'email/receiver'
require 'jobs/scheduled/poll_mailbox'
require 'email/message_builder'
describe Jobs::PollMailbox do
describe "processing email" do
let!(:poller) { Jobs::PollMailbox.new }
let!(:receiver) { mock }
let!(:email) { mock }
before do
Email::Receiver.expects(:new).with(email).returns(receiver)
end
describe "all goes fine" do
it "email gets deleted" do
receiver.expects(:process)
email.expects(:delete)
poller.handle_mail(email)
end
end
describe "raises Untrusted error" do
before do
receiver.expects(:process).raises(Email::Receiver::UserNotSufficientTrustLevelError)
email.expects(:delete)
Mail::Message.expects(:new).returns(email)
email.expects(:from)
email.expects(:body)
clientMessage = mock
senderMock = mock
RejectionMailer.expects(:send_trust_level).returns(clientMessage)
Email::Sender.expects(:new).with(
clientMessage, :email_reject_trust_level).returns(senderMock)
senderMock.expects(:send)
end
it "sends a reply and deletes the email" do
poller.handle_mail(email)
end
end
describe "raises error" do
it "deletes email on ProcessingError" do
receiver.expects(:process).raises(Email::Receiver::ProcessingError)
email.expects(:delete)
poller.handle_mail(email)
end
it "deletes email on EmailUnparsableError" do
receiver.expects(:process).raises(Email::Receiver::EmailUnparsableError)
email.expects(:delete)
poller.handle_mail(email)
end
it "deletes email on EmptyEmailError" do
receiver.expects(:process).raises(Email::Receiver::EmptyEmailError)
email.expects(:delete)
poller.handle_mail(email)
end
it "deletes email on UserNotFoundError" do
receiver.expects(:process).raises(Email::Receiver::UserNotFoundError)
email.expects(:delete)
poller.handle_mail(email)
end
it "deletes email on EmailLogNotFound" do
receiver.expects(:process).raises(Email::Receiver::EmailLogNotFound)
email.expects(:delete)
poller.handle_mail(email)
end
it "informs admins on any other error" do
receiver.expects(:process).raises(TypeError)
email.expects(:delete)
GroupMessage.expects(:create) do |args|
args[0].should eq "admins"
args[1].shouled eq :email_error_notification
args[2].message_params.source.should eq email
args[2].message_params.error.should_be instance_of(TypeError)
end
poller.handle_mail(email)
end
end
end
end

View file

@ -10,23 +10,13 @@ describe Email::Receiver do
SiteSetting.stubs(:email_in).returns(false) SiteSetting.stubs(:email_in).returns(false)
end end
describe "exception raised" do
it "returns error if it encountered an error processing" do
receiver = Email::Receiver.new("some email")
def receiver.parse_body
raise "ERROR HAPPENED!"
end
expect(receiver.process).to eq(Email::Receiver.results[:error])
end
end
describe 'invalid emails' do describe 'invalid emails' do
it "returns unprocessable if the message is blank" do it "raises EmptyEmailError if the message is blank" do
expect(Email::Receiver.new("").process).to eq(Email::Receiver.results[:unprocessable]) expect { Email::Receiver.new("").process }.to raise_error(Email::Receiver::EmptyEmailError)
end end
it "returns unprocessable if the message is not an email" do it "raises EmailUnparsableError if the message is not an email" do
expect(Email::Receiver.new("asdf" * 30).process).to eq(Email::Receiver.results[:unprocessable]) expect { Email::Receiver.new("asdf" * 30).process}.to raise_error(Email::Receiver::EmptyEmailError)
end end
end end
@ -35,7 +25,7 @@ describe Email::Receiver do
let(:receiver) { Email::Receiver.new(reply_below) } let(:receiver) { Email::Receiver.new(reply_below) }
it "processes correctly" do it "processes correctly" do
receiver.process expect { receiver.process}.to raise_error(Email::Receiver::ProcessingError)
expect(receiver.body).to eq( expect(receiver.body).to eq(
"So presumably all the quoted garbage and my (proper) signature will get "So presumably all the quoted garbage and my (proper) signature will get
stripped from my reply?") stripped from my reply?")
@ -47,7 +37,7 @@ stripped from my reply?")
let(:receiver) { Email::Receiver.new(reply_below) } let(:receiver) { Email::Receiver.new(reply_below) }
it "processes correctly" do it "processes correctly" do
receiver.process expect { receiver.process}.to raise_error(Email::Receiver::ProcessingError)
expect(receiver.body).to eq("The EC2 instance - I've seen that there tends to be odd and " + 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.") "unrecommended settings on the Bitnami installs that I've checked out.")
end end
@ -58,7 +48,7 @@ stripped from my reply?")
let(:receiver) { Email::Receiver.new(attachment) } let(:receiver) { Email::Receiver.new(attachment) }
it "processes correctly" do it "processes correctly" do
expect(receiver.process).to eq(Email::Receiver.results[:unprocessable]) expect { receiver.process}.to raise_error(Email::Receiver::EmptyEmailError)
expect(receiver.body).to be_blank expect(receiver.body).to be_blank
end end
end end
@ -68,7 +58,7 @@ stripped from my reply?")
let(:receiver) { Email::Receiver.new(dutch) } let(:receiver) { Email::Receiver.new(dutch) }
it "processes correctly" do it "processes correctly" do
receiver.process expect { receiver.process}.to raise_error(Email::Receiver::ProcessingError)
expect(receiver.body).to eq("Dit is een antwoord in het Nederlands.") expect(receiver.body).to eq("Dit is een antwoord in het Nederlands.")
end end
end end
@ -79,7 +69,7 @@ stripped from my reply?")
it "processes correctly" do it "processes correctly" do
I18n.expects(:t).with('user_notifications.previous_discussion').returns('כלטוב') I18n.expects(:t).with('user_notifications.previous_discussion').returns('כלטוב')
receiver.process expect { receiver.process}.to raise_error(Email::Receiver::ProcessingError)
expect(receiver.body).to eq("שלום") expect(receiver.body).to eq("שלום")
end end
end end
@ -90,7 +80,7 @@ stripped from my reply?")
it "processes correctly" do it "processes correctly" do
I18n.expects(:t).with('user_notifications.previous_discussion').returns('媽!我上電視了!') I18n.expects(:t).with('user_notifications.previous_discussion').returns('媽!我上電視了!')
receiver.process expect { receiver.process}.to raise_error(Email::Receiver::ProcessingError)
expect(receiver.body).to eq("媽!我上電視了!") expect(receiver.body).to eq("媽!我上電視了!")
end end
end end
@ -100,7 +90,7 @@ stripped from my reply?")
let(:receiver) { Email::Receiver.new(wrote) } let(:receiver) { Email::Receiver.new(wrote) }
it "removes via lines if we know them" do it "removes via lines if we know them" do
receiver.process expect { receiver.process}.to raise_error(Email::Receiver::ProcessingError)
expect(receiver.body).to eq("Hello this email has content!") expect(receiver.body).to eq("Hello this email has content!")
end end
end end
@ -110,7 +100,7 @@ stripped from my reply?")
let(:receiver) { Email::Receiver.new(wrote) } let(:receiver) { Email::Receiver.new(wrote) }
it "processes correctly" do it "processes correctly" do
receiver.process expect { receiver.process}.to raise_error(Email::Receiver::ProcessingError)
expect(receiver.body).to eq("Thanks!") expect(receiver.body).to eq("Thanks!")
end end
end end
@ -120,7 +110,7 @@ stripped from my reply?")
let(:receiver) { Email::Receiver.new(previous) } let(:receiver) { Email::Receiver.new(previous) }
it "processes correctly" do it "processes correctly" do
receiver.process expect { receiver.process}.to raise_error(Email::Receiver::ProcessingError)
expect(receiver.body).to eq("This will not include the previous discussion that is present in this email.") expect(receiver.body).to eq("This will not include the previous discussion that is present in this email.")
end end
end end
@ -130,7 +120,7 @@ stripped from my reply?")
let(:receiver) { Email::Receiver.new(paragraphs) } let(:receiver) { Email::Receiver.new(paragraphs) }
it "processes correctly" do it "processes correctly" do
receiver.process expect { receiver.process}.to raise_error(Email::Receiver::ProcessingError)
expect(receiver.body).to eq( expect(receiver.body).to eq(
"Is there any reason the *old* candy can't be be kept in silos while the new candy "Is there any reason the *old* candy can't be be kept in silos while the new candy
is imported into *new* silos? is imported into *new* silos?
@ -161,10 +151,8 @@ greatest show ever created. Everyone should watch it.
EmailLog.expects(:for).returns(nil) EmailLog.expects(:for).returns(nil)
end end
let!(:result) { receiver.process } it "raises EmailLogNotFoundError" do
expect{ receiver.process }.to raise_error(Email::Receiver::EmailLogNotFound)
it "returns missing" do
expect(result).to eq(Email::Receiver.results[:missing])
end end
end end
@ -185,10 +173,6 @@ greatest show ever created. Everyone should watch it.
let!(:result) { receiver.process } let!(:result) { receiver.process }
it "returns a processed result" do
expect(result).to eq(Email::Receiver.results[:processed])
end
it "extracts the body" do it "extracts the body" do
expect(receiver.body).to eq(reply_body) expect(receiver.body).to eq(reply_body)
end end
@ -229,10 +213,8 @@ Jakie" }
User.expects(:find_by_email).returns(nil) User.expects(:find_by_email).returns(nil)
end end
let!(:result) { receiver.process } it "raises user not found error" do
expect { receiver.process }.to raise_error(Email::Receiver::UserNotFoundError)
it "returns unprocessable" do
expect(result).to eq(Email::Receiver.results[:unprocessable])
end end
end end
@ -244,10 +226,8 @@ Jakie" }
SiteSetting.stubs(:email_in_min_trust).returns(TrustLevel.levels[:elder].to_s) SiteSetting.stubs(:email_in_min_trust).returns(TrustLevel.levels[:elder].to_s)
end end
let!(:result) { receiver.process } it "raises untrusted user error" do
expect { receiver.process }.to raise_error(Email::Receiver::UserNotSufficientTrustLevelError)
it "returns unprocessable" do
expect(result).to eq(Email::Receiver.results[:unprocessable])
end end
end end
@ -289,10 +269,6 @@ Jakie" }
let!(:result) { receiver.process } let!(:result) { receiver.process }
it "returns a processed result" do
expect(result).to eq(Email::Receiver.results[:processed])
end
it "extracts the body" do it "extracts the body" do
expect(receiver.body).to eq(email_body) expect(receiver.body).to eq(email_body)
end end
@ -327,10 +303,8 @@ Jakie" }
Category.expects(:find_by_email).returns(nil) Category.expects(:find_by_email).returns(nil)
end end
let!(:result) { receiver.process } it "raises EmailLogNotFoundError" do
expect{ receiver.process }.to raise_error(Email::Receiver::EmailLogNotFound)
it "returns missing" do
expect(result).to eq(Email::Receiver.results[:missing])
end end
end end
@ -343,10 +317,8 @@ Jakie" }
"discourse-in@appmail.adventuretime.ooo").returns(category) "discourse-in@appmail.adventuretime.ooo").returns(category)
end end
let!(:result) { receiver.process } it "raises UserNotFoundError" do
expect{ receiver.process }.to raise_error(Email::Receiver::UserNotFoundError)
it "returns unprocessable" do
expect(result).to eq(Email::Receiver.results[:unprocessable])
end end
end end
@ -360,10 +332,8 @@ Jakie" }
SiteSetting.stubs(:email_in_min_trust).returns(TrustLevel.levels[:elder].to_s) SiteSetting.stubs(:email_in_min_trust).returns(TrustLevel.levels[:elder].to_s)
end end
let!(:result) { receiver.process } it "raises untrusted user error" do
expect { receiver.process }.to raise_error(Email::Receiver::UserNotSufficientTrustLevelError)
it "returns unprocessable" do
expect(result).to eq(Email::Receiver.results[:unprocessable])
end end
end end
@ -408,10 +378,6 @@ Jakie" }
let!(:result) { receiver.process } let!(:result) { receiver.process }
it "returns a processed result" do
expect(result).to eq(Email::Receiver.results[:processed])
end
it "extracts the body" do it "extracts the body" do
expect(receiver.body).to eq(email_body) expect(receiver.body).to eq(email_body)
end end
@ -449,10 +415,8 @@ Jakie
"discourse-in@appmail.adventuretime.ooo").returns(non_inbox_email_category) "discourse-in@appmail.adventuretime.ooo").returns(non_inbox_email_category)
end end
let!(:result) { receiver.process } it "raises UserNotFoundError" do
expect{ receiver.process }.to raise_error(Email::Receiver::UserNotFoundError)
it "returns unprocessable" do
expect(result).to eq(Email::Receiver.results[:unprocessable])
end end
end end
@ -495,10 +459,6 @@ Jakie
let!(:result) { receiver.process } let!(:result) { receiver.process }
it "returns a processed result" do
expect(result).to eq(Email::Receiver.results[:processed])
end
it "extracts the body" do it "extracts the body" do
expect(receiver.body).to eq(email_body) expect(receiver.body).to eq(email_body)
end end