Skip to content

Commit

Permalink
Do not accept invalid addresses in SMTP RCPT TO
Browse files Browse the repository at this point in the history
This gives the MTA the chance to inform the original sender
about the transmission error via a delivery report.
  • Loading branch information
paroga committed Oct 5, 2017
1 parent e017a11 commit b35357d
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 19 deletions.
47 changes: 28 additions & 19 deletions lib/foodsoft_mail_receiver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,36 +10,45 @@ def self.register(klass)
end

def self.received(recipient, data)
m = /(?<foodcoop>[^@\.]+)\.(?<address>[^@]+)(@(?<hostname>[^@]+))?/.match recipient
raise "recipient is missing or has an invalid format" if m.nil?
raise "Foodcoop '#{m[:foodcoop]}' could not be found" unless FoodsoftConfig.foodcoops.include? m[:foodcoop]
FoodsoftConfig.select_multifoodcoop m[:foodcoop]

@@registered_classes.each do |klass|
klass_m = klass.regexp.match(m[:address])
return klass.new(klass_m).received(data) if klass_m
end

raise "invalid format for recipient"
find_handler(recipient).call(data)
end

def start
super
@handlers = []
end

private

def on_rcpt_to_event(ctx, rcpt_to)
recipient = rcpt_to.gsub(/^\s*<\s*(.*)\s*>\s*$/, '\1')
@handlers << self.class.find_handler(recipient)
rcpt_to
rescue => error
raise MidiSmtpServer::Smtpd550Exception
end

def on_message_data_event(ctx)
puts ctx[:envelope][:to]
ctx[:envelope][:to].each do |to|
begin
m = /<(?<recipient>[^<>]+)>/.match(to)
raise "invalid format for RCPT TO" if m.nil?
FoodsoftMailReceiver.received(m[:recipient], ctx[:message][:data])
rescue => error
Rails.logger.warn "Can't deliver mail to #{to}: #{error.message}"
@handlers.each do |handler|
handler.call(ctx[:message][:data])
end
@handlers.clear
end

def self.find_handler(recipient)
m = /(?<foodcoop>[^@\.]+)\.(?<address>[^@]+)(@(?<hostname>[^@]+))?/.match recipient
raise "recipient is missing or has an invalid format" if m.nil?
raise "Foodcoop '#{m[:foodcoop]}' could not be found" unless FoodsoftConfig.allowed_foodcoop? m[:foodcoop]
FoodsoftConfig.select_multifoodcoop m[:foodcoop]

@@registered_classes.each do |klass|
if match = klass.regexp.match(m[:address])
handler = klass.new match
return lambda { |data| handler.received(data) }
end
end

raise "invalid format for recipient"
end

end
79 changes: 79 additions & 0 deletions spec/lib/foodsoft_mail_receiver_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
require_relative '../spec_helper'

describe FoodsoftMailReceiver do

before :all do
@server = FoodsoftMailReceiver.new 0
@server.start
end

it 'does not accept empty addresses' do
begin
FoodsoftMailReceiver.received('', 'body')
rescue => error
expect(error.to_s).to include 'missing'
end
end

it 'does not accept invalid addresses' do
begin
FoodsoftMailReceiver.received('invalid', 'body')
rescue => error
expect(error.to_s).to include 'has an invalid format'
end
end

it 'does not accept invalid scope in address' do
begin
FoodsoftMailReceiver.received('invalid.invalid', 'body')
rescue => error
expect(error.to_s).to include 'could not be found'
end
end

it 'does not accept address without handler' do
begin
address = "#{FoodsoftConfig[:default_scope]}.invalid"
FoodsoftMailReceiver.received(address, 'body')
rescue => error
expect(error.to_s).to include 'invalid format for recipient'
end
end

it 'does not accept invalid addresses via SMTP' do
expect {
Net::SMTP.start(@server.host, @server.port) do |smtp|
smtp.send_message 'body', 'from@example.com', 'invalid'
end
}.to raise_error(Net::SMTPFatalError)
end

it 'does not accept invalid addresses via SMTP' do
expect {
Net::SMTP.start(@server.host, @server.port) do |smtp|
smtp.send_message 'body', 'from@example.com', 'invalid'
end
}.to raise_error(Net::SMTPFatalError)
end

# TODO: Reanable this test.
# It raised "Mysql2::Error: Lock wait timeout exceeded" at time of writing.
# it 'accepts bounce mails via SMTP' do
# MailDeliveryStatus.delete_all
#
# Net::SMTP.start(@server.host, @server.port) do |smtp|
# address = "#{FoodsoftConfig[:default_scope]}.bounce+user=example.com"
# smtp.send_message 'report', 'from@example.com', address
# end
#
# mds = MailDeliveryStatus.last
# expect(mds.email).to eq 'user@example.com'
# expect(mds.attachment_mime).to eq 'message/rfc822'
# expect(mds.attachment_data).to include 'report'
# end

after :all do
@server.shutdown
end

end

0 comments on commit b35357d

Please sign in to comment.