verify incoming mail sender addresses

fixes CNVS-20518

test plan
- receive a notification from canvas that you can reply to
 (conversation, discussion)
- forward the email to a third party with a different email address
- as the third party, send a reply to the canvas incoming email
 address
- ensure that a reply is not posted in canvas

Change-Id: Ibc79c591d2f231bce519dd056d02a516a3d336c7
Reviewed-on: https://gerrit.instructure.com/54628
Tested-by: Jenkins
Reviewed-by: Matthew Wheeler <mwheeler@instructure.com>
QA-Review: Steven Shepherd <sshepherd@instructure.com>
Product-Review: Joel Hough <joel@instructure.com>
This commit is contained in:
Joel Hough 2015-05-19 16:21:00 -06:00
parent 47aac61fdb
commit a9d021c941
3 changed files with 54 additions and 24 deletions

View File

@ -3,6 +3,7 @@ module IncomingMail
class SilentIgnore < StandardError; end class SilentIgnore < StandardError; end
class ReplyFrom < StandardError; end class ReplyFrom < StandardError; end
class UnknownAddress < ReplyFrom; end class UnknownAddress < ReplyFrom; end
class UnknownSender < ReplyFrom; end
class ReplyToLockedTopic < ReplyFrom; end class ReplyToLockedTopic < ReplyFrom; end
end end
end end

View File

@ -28,11 +28,13 @@ module IncomingMail
raise IncomingMail::Errors::SilentIgnore unless original_message && original_message.notification_id raise IncomingMail::Errors::SilentIgnore unless original_message && original_message.notification_id
raise IncomingMail::Errors::SilentIgnore unless valid_secure_id?(original_message, secure_id) raise IncomingMail::Errors::SilentIgnore unless valid_secure_id?(original_message, secure_id)
from_channel = nil
original_message.shard.activate do original_message.shard.activate do
context = original_message.context context = original_message.context
user = original_message.user user = original_message.user
raise IncomingMail::Errors::UnknownAddress unless valid_user_and_context?(context, user) raise IncomingMail::Errors::UnknownAddress unless valid_user_and_context?(context, user)
from_channel = sent_from_channel(user, incoming_message)
raise IncomingMail::Errors::UnknownSender unless from_channel
context.reply_from({ context.reply_from({
:purpose => 'general', :purpose => 'general',
:user => user, :user => user,
@ -42,15 +44,15 @@ module IncomingMail
}) })
end end
rescue IncomingMail::Errors::ReplyFrom => error rescue IncomingMail::Errors::ReplyFrom => error
bounce_message(original_message, incoming_message, error, outgoing_from_address) bounce_message(original_message, incoming_message, error, outgoing_from_address, from_channel)
rescue IncomingMail::Errors::SilentIgnore rescue IncomingMail::Errors::SilentIgnore
#do nothing #do nothing
end end
private private
def bounce_message(original_message, incoming_message, error, outgoing_from_address) def bounce_message(original_message, incoming_message, error, outgoing_from_address, from_channel)
incoming_from = incoming_message.from.try(:first) incoming_from = from_channel.try(:path) || incoming_message.from.try(:first)
incoming_subject = incoming_message.subject incoming_subject = incoming_message.subject
return unless incoming_from return unless incoming_from
@ -69,7 +71,7 @@ module IncomingMail
outgoing_message_delivered = false outgoing_message_delivered = false
original_message.shard.activate do original_message.shard.activate do
comch = CommunicationChannel.active.email.by_path(incoming_from).first comch = from_channel || CommunicationChannel.active.email.by_path(incoming_from).first
outgoing_message.communication_channel = comch outgoing_message.communication_channel = comch
outgoing_message.user = comch.try(:user) outgoing_message.user = comch.try(:user)
if outgoing_message.communication_channel if outgoing_message.communication_channel
@ -98,6 +100,14 @@ module IncomingMail
ndr_body = I18n.t('lib.incoming_message_processor.locked_topic.body', <<-BODY, :subject => subject).gsub(/^ +/, '') ndr_body = I18n.t('lib.incoming_message_processor.locked_topic.body', <<-BODY, :subject => subject).gsub(/^ +/, '')
The message titled "%{subject}" could not be delivered because the discussion topic is locked. If you are trying to contact someone through Canvas you can try logging in to your account and sending them a message using the Inbox tool. The message titled "%{subject}" could not be delivered because the discussion topic is locked. If you are trying to contact someone through Canvas you can try logging in to your account and sending them a message using the Inbox tool.
Thank you,
Canvas Support
BODY
when IncomingMail::Errors::UnknownSender
ndr_subject = I18n.t("Message Reply Failed: %{subject}", :subject => subject)
ndr_body = I18n.t(<<-BODY, :subject => subject).gsub(/^ +/, '')
The message titled "%{subject}" could not be delivered. The message was sent from an address that is not linked with your Canvas account. If you are trying to contact someone through Canvas you can try logging in to your account and sending them a message using the Inbox tool.
Thank you, Thank you,
Canvas Support Canvas Support
BODY BODY
@ -122,6 +132,11 @@ module IncomingMail
user && context && context.respond_to?(:reply_from) user && context && context.respond_to?(:reply_from)
end end
def sent_from_channel(user, incoming_message)
from_addresses = ((incoming_message.from || []) + (incoming_message.reply_to || [])).uniq
user && from_addresses.lazy.map {|addr| user.communication_channels.active.email.by_path(addr).first}.first
end
# MOVE! # MOVE!
def utf8ify(string, encoding) def utf8ify(string, encoding)
encoding ||= 'UTF-8' encoding ||= 'UTF-8'

View File

@ -25,13 +25,18 @@ describe IncomingMail::MessageHandler do
let(:original_message_id) { 1 } let(:original_message_id) { 1 }
let(:secure_id) { "123abc" } let(:secure_id) { "123abc" }
let(:tag) { "#{secure_id}-#{original_message_id}" } let(:tag) { "#{secure_id}-#{original_message_id}" }
let(:shard) do let(:shard) do
stub("shard").tap do |shard| stub("shard").tap do |shard|
shard.stubs(:activate).yields shard.stubs(:activate).yields
end end
end end
let(:user) { stub("user") } let_once(:user) do
let(:context) { stub("context", reply_from: "reply-from@example.com") } user_model
channel = @user.communication_channels.create!(:path => "lucy@example.com", :path_type => "email")
channel.confirm!
@user
end
let(:context) { stub("context") }
let(:original_message_attributes) { let(:original_message_attributes) {
{ {
@ -39,7 +44,8 @@ describe IncomingMail::MessageHandler do
:shard => shard, :shard => shard,
:context => context, :context => context,
:user => user, :user => user,
:global_id => 1 :global_id => 1,
:to => "lucy@example.com"
} }
} }
@ -49,7 +55,8 @@ describe IncomingMail::MessageHandler do
header: { header: {
:subject => stub("subject", :charset => "utf8") :subject => stub("subject", :charset => "utf8")
}, },
from: ["lucy@example.com"] from: ["lucy@example.com"],
reply_to: ["lucy@example.com"]
} }
} }
@ -89,6 +96,7 @@ describe IncomingMail::MessageHandler do
end end
it "silently fails on invalid secure id" do it "silently fails on invalid secure id" do
Message.stubs(:where).with(id: original_message_id).returns(stub(first: original_message))
Canvas::Security.stubs(:verify_hmac_sha1).returns(false) Canvas::Security.stubs(:verify_hmac_sha1).returns(false)
Mailer.expects(:create_message).never Mailer.expects(:create_message).never
@ -109,16 +117,23 @@ describe IncomingMail::MessageHandler do
Message.any_instance.expects(:deliver).never Message.any_instance.expects(:deliver).never
subject.handle(outgoing_from_address, body, html_body, incoming_message, "#{secure_id}-not-an-id") subject.handle(outgoing_from_address, body, html_body, incoming_message, "#{secure_id}-not-an-id")
end end
it "silently fails if the message is not from one of the original recipient's email addresses" do
Message.stubs(:where).with(id: original_message_id).returns(stub(first: original_message))
Message.any_instance.expects(:deliver).never
original_message.context.expects(:reply_from).never
message = stub("incoming message with bad from",
incoming_message_attributes.merge(:from => ['not_lucy@example.com'],
:reply_to => ['also_not_lucy@example.com']))
subject.handle(outgoing_from_address, body, html_body, message, tag)
end
end end
context "bounced messages" do context "bounced messages" do
it "bounces the message if user is missing" do it "bounces if user is missing" do
message = stub("original message without user", original_message_attributes.merge(:user => nil)) message = stub("original message without user", original_message_attributes.merge(:user => nil))
Message.stubs(:where).with(id: original_message_id).returns(stub(first: message)) Message.stubs(:where).with(id: original_message_id).returns(stub(first: message))
Message.any_instance.expects(:deliver)
Message.any_instance.expects(:deliver).never
Mailer.expects(:create_message)
subject.handle(outgoing_from_address, body, html_body, incoming_message, tag) subject.handle(outgoing_from_address, body, html_body, incoming_message, tag)
end end
@ -126,16 +141,13 @@ describe IncomingMail::MessageHandler do
message = stub("original message with invalid context", original_message_attributes.merge({context: stub("context")})) message = stub("original message with invalid context", original_message_attributes.merge({context: stub("context")}))
Message.stubs(:where).with(id: original_message_id).returns(stub(first: message)) Message.stubs(:where).with(id: original_message_id).returns(stub(first: message))
Message.any_instance.expects(:deliver).never Message.any_instance.expects(:save)
Mailer.expects(:create_message) Message.any_instance.expects(:deliver)
subject.handle(outgoing_from_address, body, html_body, incoming_message, tag) subject.handle(outgoing_from_address, body, html_body, incoming_message, tag)
end end
it "saves and delivers the message with proper input" do it "saves and delivers the message with proper input" do
user_model
channel = @user.communication_channels.create!(:path => "lucy@example.com", :path_type => "email")
channel.confirm!
message = stub("original message with invalid context", original_message_attributes.merge({context: stub("context")})) message = stub("original message with invalid context", original_message_attributes.merge({context: stub("context")}))
Message.stubs(:where).with(id: original_message_id).returns(stub(first: message)) Message.stubs(:where).with(id: original_message_id).returns(stub(first: message))
@ -156,7 +168,7 @@ describe IncomingMail::MessageHandler do
context "with a generic generic_error" do context "with a generic generic_error" do
it "constructs the message correctly" do it "constructs the message correctly" do
message = stub("original message without user", original_message_attributes.merge(:user => nil)) message = stub("original message without user", original_message_attributes.merge(:context => nil))
Message.stubs(:where).with(id: original_message_id).returns(stub(first: message)) Message.stubs(:where).with(id: original_message_id).returns(stub(first: message))
email_subject = "Message Reply Failed: some subject" email_subject = "Message Reply Failed: some subject"
@ -247,12 +259,14 @@ describe IncomingMail::MessageHandler do
context "when there is no communication channel" do context "when there is no communication channel" do
it "bounces the message back to the incoming from address" do it "bounces the message back to the incoming from address" do
Message.stubs(:where).with(id: original_message_id).returns(stub(first: original_message)) Message.stubs(:where).with(id: original_message_id).returns(stub(first: original_message))
context.expects(:reply_from).raises(IncomingMail::Errors::ReplyToLockedTopic.new)
Message.any_instance.expects(:deliver).never Message.any_instance.expects(:deliver).never
Mailer.expects(:create_message) Mailer.expects(:create_message)
subject.handle(outgoing_from_address, body, html_body, incoming_message, tag) message = stub("incoming message with bad from",
incoming_message_attributes.merge(:from => ['not_lucy@example.com'],
:reply_to => ['also_not_lucy@example.com']))
subject.handle(outgoing_from_address, body, html_body, message, tag)
end end
end end
end end