1
0
مراية لـ https://github.com/postalserver/postal.git تم المزامنة 2025-11-30 21:32:30 +00:00

test: add initial tests for Postal::SMTPServer::Client

هذا الالتزام موجود في:
Adam Cooke
2024-02-12 18:07:44 +00:00
الأصل ec636661d5
التزام dece1d487a
19 ملفات معدلة مع 780 إضافات و44 حذوفات

عرض الملف

@@ -10,6 +10,7 @@ AllCops:
# Always use double quotes
Style/StringLiterals:
EnforcedStyle: double_quotes
AutoCorrect: true
# We prefer arrays of symbols to look like an array of symbols.
# For example: [:one, :two, :three] as opposed to %i[one two three]

عرض الملف

@@ -48,9 +48,10 @@ end
group :development do
gem "annotate"
gem "database_cleaner", require: false
gem "factory_bot_rails", "~> 4.0", require: false
gem "factory_bot_rails", require: false
gem "rspec", require: false
gem "rspec-rails", require: false
gem "rubocop"
gem "rubocop-rails"
gem "timecop"
end

عرض الملف

@@ -89,7 +89,7 @@ GEM
coffee-script-source
execjs
coffee-script-source (1.12.2)
concurrent-ruby (1.2.2)
concurrent-ruby (1.2.3)
crass (1.0.6)
database_cleaner (2.0.2)
database_cleaner-active_record (>= 2, < 3)
@@ -104,15 +104,17 @@ GEM
dotenv-rails (2.8.1)
dotenv (= 2.8.1)
railties (>= 3.2)
dynamic_form (1.1.4)
dynamic_form (1.3.1)
actionview (> 5.2.0)
activemodel (> 5.2.0)
encrypto_signo (1.0.0)
erubi (1.12.0)
execjs (2.7.0)
factory_bot (4.11.1)
activesupport (>= 3.0.0)
factory_bot_rails (4.11.1)
factory_bot (~> 4.11.1)
railties (>= 3.0.0)
factory_bot (6.4.6)
activesupport (>= 5.0.0)
factory_bot_rails (6.4.3)
factory_bot (~> 6.4)
railties (>= 5.0.0)
ffi (1.15.5)
foreman (0.87.2)
gelf (3.1.0)
@@ -125,7 +127,7 @@ GEM
tilt
hashie (5.0.0)
highline (2.1.0)
i18n (1.12.0)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
jquery-rails (4.5.1)
rails-dom-testing (>= 1, < 3)
@@ -145,9 +147,9 @@ GEM
activerecord
kaminari-core (= 1.2.2)
kaminari-core (1.2.2)
loofah (2.19.1)
loofah (2.22.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
nokogiri (>= 1.12.0)
mail (2.8.1)
mini_mime (>= 0.1.1)
net-imap
@@ -156,8 +158,7 @@ GEM
marcel (1.0.2)
method_source (1.0.0)
mini_mime (1.1.2)
mini_portile2 (2.8.5)
minitest (5.18.0)
minitest (5.22.2)
moonrope (2.0.2)
deep_merge (~> 1.0)
json
@@ -177,9 +178,6 @@ GEM
activerecord (>= 4.0.0)
activesupport (>= 4.0.0)
nio4r (2.7.0)
nokogiri (1.16.2)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.16.2-arm64-darwin)
racc (~> 1.4)
nokogiri (1.16.2-x86_64-linux)
@@ -190,7 +188,7 @@ GEM
puma (6.4.2)
nio4r (~> 2.0)
racc (1.7.3)
rack (2.2.6.4)
rack (2.2.8)
rack-test (2.1.0)
rack (>= 1.3)
rails (6.1.7.6)
@@ -208,11 +206,13 @@ GEM
bundler (>= 1.15.0)
railties (= 6.1.7.6)
sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.5.0)
loofah (~> 2.19, >= 2.19.1)
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
nokogiri (~> 1.14)
railties (6.1.7.6)
actionpack (= 6.1.7.6)
activesupport (= 6.1.7.6)
@@ -220,7 +220,7 @@ GEM
rake (>= 12.2)
thor (~> 1.0)
rainbow (3.1.1)
rake (13.0.6)
rake (13.1.0)
rbtree (0.4.6)
regexp_parser (2.7.0)
resolv (0.2.2)
@@ -292,8 +292,9 @@ GEM
activesupport (>= 5.2)
sprockets (>= 3.0.0)
temple (0.10.0)
thor (1.2.1)
thor (1.3.0)
tilt (2.1.0)
timecop (0.9.8)
timeout (0.3.2)
turbolinks (5.2.1)
turbolinks-source (~> 5.2)
@@ -306,11 +307,11 @@ GEM
websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
zeitwerk (2.6.7)
zeitwerk (2.6.13)
PLATFORMS
arm64-darwin-22
ruby
arm64-darwin-23
x86_64-linux
DEPENDENCIES
@@ -330,7 +331,7 @@ DEPENDENCIES
dynamic_form
encrypto_signo
execjs (~> 2.7, < 2.8)
factory_bot_rails (~> 4.0)
factory_bot_rails
foreman
gelf
haml
@@ -356,6 +357,7 @@ DEPENDENCIES
secure_headers
sentry-rails
sentry-ruby
timecop
turbolinks (~> 5)
uglifier (>= 1.3.0)

عرض الملف

@@ -25,7 +25,7 @@ module Postal
@attributes[name.to_s]
end
def respond_to_missing?(name)
def respond_to_missing?(name, include_private = false)
@attributes.key?(name.to_s)
end

عرض الملف

@@ -185,7 +185,7 @@ module Postal
end
end
def respond_to_missing?(name)
def respond_to_missing?(name, include_private = false)
name = name.to_s.sub(/=\z/, "")
@attributes.key?(name.to_s)
end

عرض الملف

@@ -11,6 +11,12 @@ module Postal
LOG_REDACTION_STRING = "[redacted]"
attr_reader :logging_enabled
attr_reader :credential
attr_reader :ip_address
attr_reader :recipients
attr_reader :headers
attr_reader :state
attr_reader :helo_name
def initialize(ip_address)
@logging_enabled = true
@@ -55,19 +61,6 @@ module Postal
end
end
def sanitize_input_for_log(data)
if @password_expected_next
@password_expected_next = false
if data =~ /\A[a-z0-9]{3,}=*\z/i
return LOG_REDACTION_STRING
end
end
data = data.dup
data.gsub!(/(.*AUTH \w+) (.*)\z/i) { "#{::Regexp.last_match(1)} #{LOG_REDACTION_STRING}" }
data
end
def finished?
@finished || false
end
@@ -137,7 +130,11 @@ module Postal
@helo_name = data.strip.split(" ", 2)[1]
transaction_reset
@state = :welcomed
["250-My capabilities are", Postal.config.smtp_server.tls_enabled? && !@tls ? "250-STARTTLS" : nil, "250 AUTH CRAM-MD5 PLAIN LOGIN"]
[
"250-My capabilities are",
Postal.config.smtp_server.tls_enabled? && !@tls ? "250-STARTTLS" : nil,
"250 AUTH CRAM-MD5 PLAIN LOGIN"
].compact
end
def helo(data)
@@ -194,7 +191,7 @@ module Postal
"334 UGFzc3dvcmQ6" # "Password:"
end
data = data.gsub!(/AUTH LOGIN ?/i, "")
data = data.gsub(/AUTH LOGIN ?/i, "")
if data.strip == ""
@proc = username_handler
"334 VXNlcm5hbWU6" # "Username:"
@@ -226,6 +223,7 @@ module Postal
log "\e[33m WARN: AUTH failure for #{@ip_address}\e[0m"
next "535 Denied"
end
grant = nil
server.credentials.where(type: "SMTP").each do |credential|
correct_response = OpenSSL::HMAC.hexdigest(CRAM_MD5_DIGEST, credential.key, challenge)
@@ -236,10 +234,12 @@ module Postal
grant = "235 Granted for #{credential.server.organization.permalink}/#{credential.server.permalink}"
break
end
if grant.nil?
log "\e[33m WARN: AUTH failure for #{@ip_address}\e[0m"
next "535 Denied"
end
grant
end
@@ -458,6 +458,7 @@ module Postal
msg.mail_from = @mail_from
msg.raw_message = @data
msg.received_with_ssl = @tls
msg.bounce = 1
end
else
# There's no return path route, we just need to insert the mesage
@@ -489,6 +490,19 @@ module Postal
states.include?(@state)
end
def sanitize_input_for_log(data)
if @password_expected_next
@password_expected_next = false
if data =~ /\A[a-z0-9]{3,}=*\z/i
return LOG_REDACTION_STRING
end
end
data = data.dup
data.gsub!(/(.*AUTH \w+) (.*)\z/i) { "#{::Regexp.last_match(1)} #{LOG_REDACTION_STRING}" }
data
end
end
end
end

عرض الملف

@@ -0,0 +1,9 @@
# frozen_string_literal: true
FactoryBot.define do
factory :credential do
server
name { "Example Credential" }
type { "API" }
end
end

عرض الملف

@@ -38,7 +38,7 @@
FactoryBot.define do
factory :domain do
association :owner, factory: :user
association :owner, factory: :organization
sequence(:name) { |n| "example#{n}.com" }
verification_method { "DNS" }
verified_at { Time.now }

عرض الملف

@@ -0,0 +1,11 @@
# frozen_string_literal: true
FactoryBot.define do
factory :http_endpoint do
server
name { "HTTP endpoint" }
url { "https://example.com/endpoint" }
encoding { "BodyAsJSON" }
format { "Hash" }
end
end

عرض الملف

@@ -0,0 +1,14 @@
# frozen_string_literal: true
FactoryBot.define do
factory :route do
name { "test" }
mode { "Accept" }
spam_mode { "Mark" }
before(:create) do |route|
route.server ||= create(:server)
route.domain ||= create(:domain, owner: route.server)
end
end
end

عرض الملف

@@ -48,5 +48,9 @@ FactoryBot.define do
mode { "Live" }
provision_database { false }
sequence(:permalink) { |n| "server#{n}" }
trait :suspended do
suspended_at { Time.current }
end
end
end

عرض الملف

@@ -0,0 +1,122 @@
# frozen_string_literal: true
require "rails_helper"
module Postal
module SMTPServer
describe Client do
let(:ip_address) { "1.2.3.4" }
subject(:client) { described_class.new(ip_address) }
before do
client.handle("HELO test.example.com")
end
describe "AUTH PLAIN" do
context "when no credentials are provided on the initial data" do
it "returns a 334" do
expect(client.handle("AUTH PLAIN")).to eq("334")
end
it "accepts the username and password from the next input" do
client.handle("AUTH PLAIN")
credential = create(:credential, type: "SMTP")
expect(client.handle(credential.to_smtp_plain)).to match(/235 Granted for/)
end
end
context "when valid credentials are provided on one line" do
it "authenticates and returns a response" do
credential = create(:credential, type: "SMTP")
expect(client.handle("AUTH PLAIN #{credential.to_smtp_plain}")).to match(/235 Granted for/)
expect(client.credential).to eq credential
end
end
context "when invalid credentials are provided" do
it "returns an error and resets the state" do
base64 = Base64.encode64("user\0pass")
expect(client.handle("AUTH PLAIN #{base64}")).to eq("535 Invalid credential")
expect(client.state).to eq :welcomed
end
end
context "when username or password is missing" do
it "returns an error and resets the state" do
base64 = Base64.encode64("pass")
expect(client.handle("AUTH PLAIN #{base64}")).to eq("535 Authenticated failed - protocol error")
expect(client.state).to eq :welcomed
end
end
end
describe "AUTH LOGIN" do
context "when no username is provided on the first line" do
it "requests the username" do
expect(client.handle("AUTH LOGIN")).to eq("334 VXNlcm5hbWU6")
end
end
context "when a username is provided on the first line" do
it "requests a password" do
username = Base64.encode64("xx")
expect(client.handle("AUTH LOGIN #{username}")).to eq("334 UGFzc3dvcmQ6")
end
it "authenticates and returns a response" do
credential = create(:credential, type: "SMTP")
username = Base64.encode64("xx")
password = Base64.encode64(credential.key)
expect(client.handle("AUTH LOGIN #{username}")).to eq("334 UGFzc3dvcmQ6")
expect(client.handle(password)).to match(/235 Granted for/)
expect(client.credential).to eq credential
end
end
context "when invalid credentials are provided" do
it "returns an error and resets the state" do
username = Base64.encode64("xx")
password = Base64.encode64("xx")
expect(client.handle("AUTH LOGIN #{username}")).to eq("334 UGFzc3dvcmQ6")
expect(client.handle(password)).to eq("535 Invalid credential")
expect(client.state).to eq :welcomed
end
end
end
describe "AUTH CRAM-MD5" do
context "when valid credentials are provided" do
it "authenticates and returns a response" do
credential = create(:credential, type: "SMTP")
result = client.handle("AUTH CRAM-MD5")
expect(result).to match(/\A334 [A-Za-z0-9=]+\z/)
challenge = Base64.decode64(result.split[1])
password = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("md5"), credential.key, challenge)
base64 = Base64.encode64("#{credential.server.organization.permalink}/#{credential.server.permalink} #{password}")
expect(client.handle(base64)).to match(/235 Granted for/)
expect(client.credential).to eq credential
end
end
context "when no org/server matches the provided username" do
it "returns an error" do
client.handle("AUTH CRAM-MD5")
base64 = Base64.encode64("org/server password")
expect(client.handle(base64)).to eq "535 Denied"
end
end
context "when invalid credentials are provided" do
it "returns an error and resets the state" do
server = create(:server)
base64 = Base64.encode64("#{server.organization.permalink}/#{server.permalink} invalid-password")
client.handle("AUTH CRAM-MD5")
expect(client.handle(base64)).to eq("535 Denied")
end
end
end
end
end
end

عرض الملف

@@ -0,0 +1,86 @@
# frozen_string_literal: true
require "rails_helper"
module Postal
module SMTPServer
describe Client do
let(:ip_address) { "1.2.3.4" }
subject(:client) { described_class.new(ip_address) }
describe "DATA" do
it "returns an error if no helo" do
expect(client.handle("DATA")).to eq "503 HELO/EHLO, MAIL FROM and RCPT TO before sending data"
end
it "returns an error if no mail from" do
client.handle("HELO test.example.com")
expect(client.handle("DATA")).to eq "503 HELO/EHLO, MAIL FROM and RCPT TO before sending data"
end
it "returns an error if no rcpt to" do
client.handle("HELO test.example.com")
client.handle("MAIL FROM: test@example.com")
expect(client.handle("DATA")).to eq "503 HELO/EHLO, MAIL FROM and RCPT TO before sending data"
end
it "returns go ahead" do
route = create(:route)
client.handle("HELO test.example.com")
client.handle("MAIL FROM: test@test.com")
client.handle("RCPT TO: #{route.name}@#{route.domain.name}")
expect(client.handle("DATA")).to eq "354 Go ahead"
end
it "adds a received header for itself" do
route = create(:route)
client.handle("HELO test.example.com")
client.handle("MAIL FROM: test@test.com")
client.handle("RCPT TO: #{route.name}@#{route.domain.name}")
Timecop.freeze do
client.handle("DATA")
expect(client.headers["received"]).to include "from test.example.com (1.2.3.4 [1.2.3.4]) by postal.example.com with SMTP; #{Time.now.utc.rfc2822}"
end
end
describe "subsequent commands" do
let(:route) { create(:route) }
before do
client.handle("HELO test.example.com")
client.handle("MAIL FROM: test@test.com")
client.handle("RCPT TO: #{route.name}@#{route.domain.name}")
client.handle("DATA")
end
it "logs headers" do
client.handle("Subject: Test")
client.handle("From: test@test.com")
client.handle("To: test1@example.com")
client.handle("To: test2@example.com")
client.handle("X-Something: abcdef1234")
expect(client.headers["subject"]).to eq ["Test"]
expect(client.headers["from"]).to eq ["test@test.com"]
expect(client.headers["to"]).to eq ["test1@example.com", "test2@example.com"]
expect(client.headers["x-something"]).to eq ["abcdef1234"]
end
it "logs content" do
client.handle("Subject: Test")
client.handle("")
client.handle("This is some content for the message.")
client.handle("It will keep going.")
expect(client.instance_variable_get("@data")).to eq <<~DATA
Received: from test.example.com (1.2.3.4 [1.2.3.4]) by #{Postal.config.dns.smtp_server_hostname} with SMTP; #{Time.now.utc.rfc2822}\r
Subject: Test\r
\r
This is some content for the message.\r
It will keep going.\r
DATA
end
end
end
end
end
end

عرض الملف

@@ -0,0 +1,212 @@
# frozen_string_literal: true
require "rails_helper"
module Postal
module SMTPServer
describe Client do
let(:ip_address) { "1.2.3.4" }
let(:server) { GLOBAL_SERVER } # We'll use the global server instance for this
subject(:client) { described_class.new(ip_address) }
let(:credential) { create(:credential, server: server, type: "SMTP") }
let(:auth_plain) { credential&.to_smtp_plain }
let(:mail_from) { "test@example.com" }
let(:rcpt_to) { "test@example.com" }
before do
client.handle("HELO test.example.com")
client.handle("AUTH PLAIN #{auth_plain}") if auth_plain
client.handle("MAIL FROM: #{mail_from}")
client.handle("RCPT TO: #{rcpt_to}")
end
after do
server.message_db.provisioner.clean
end
describe "when finished sending data" do
context "when the data is larger than the maximum message size" do
it "returns an error and resets the state" do
allow(Postal.config.smtp_server).to receive(:max_message_size).and_return(1)
client.handle("DATA")
client.handle("a" * 1024 * 1024 * 10)
expect(client.handle(".")).to eq "552 Message too large (maximum size 1MB)"
end
end
context "when a loop is detected" do
it "returns an error and resets the state" do
client.handle("DATA")
client.handle("Received: from example1.com by #{Postal.config.dns.smtp_server_hostname}")
client.handle("Received: from example2.com by #{Postal.config.dns.smtp_server_hostname}")
client.handle("Received: from example1.com by #{Postal.config.dns.smtp_server_hostname}")
client.handle("Received: from example2.com by #{Postal.config.dns.smtp_server_hostname}")
client.handle("Subject: Test")
client.handle("From: #{mail_from}")
client.handle("To: #{rcpt_to}")
client.handle("")
client.handle("This is a test message")
expect(client.handle(".")).to eq "550 Loop detected"
end
end
context "when the email content is not suitable for the credential" do
it "returns an error and resets the state" do
client.handle("DATA")
client.handle("Subject: Test")
client.handle("From: invalid@krystal.uk")
client.handle("To: #{rcpt_to}")
client.handle("")
client.handle("This is a test message")
expect(client.handle(".")).to eq "530 From/Sender name is not valid"
end
end
context "when sending an outgoing email" do
let(:domain) { create(:domain, owner: server) }
let(:mail_from) { "test@#{domain.name}" }
let(:auth_plain) { credential.to_smtp_plain }
it "stores the message and resets the state" do
client.handle("DATA")
client.handle("Subject: Test")
client.handle("From: #{mail_from}")
client.handle("To: #{rcpt_to}")
client.handle("")
client.handle("This is a test message")
expect(client.handle(".")).to eq "250 OK"
queued_message = QueuedMessage.first
expect(queued_message).to have_attributes(
domain: "example.com",
server: server
)
expect(server.message(queued_message.message_id)).to have_attributes(
mail_from: mail_from,
rcpt_to: rcpt_to,
subject: "Test",
scope: "outgoing",
route_id: nil,
credential_id: credential.id,
raw_headers: kind_of(String),
raw_message: kind_of(String)
)
end
end
context "when sending a bounce message" do
let(:credential) { nil }
let(:rcpt_to) { "#{server.token}@#{Postal.config.dns.return_path}" }
context "when there is a return path route" do
let(:domain) { create(:domain, owner: server) }
before do
endpoint = create(:http_endpoint, server: server)
create(:route, domain: domain, server: server, name: "__returnpath__", mode: "Endpoint", endpoint: endpoint)
end
it "stores the message for the return path route and resets the state" do
client.handle("DATA")
client.handle("Subject: Bounce: Test")
client.handle("From: #{mail_from}")
client.handle("To: #{rcpt_to}")
client.handle("")
client.handle("This is a test message")
expect(client.handle(".")).to eq "250 OK"
queued_message = QueuedMessage.first
expect(queued_message).to have_attributes(
domain: Postal.config.dns.return_path,
server: server
)
expect(server.message(queued_message.message_id)).to have_attributes(
mail_from: mail_from,
rcpt_to: rcpt_to,
subject: "Bounce: Test",
scope: "incoming",
route_id: server.routes.first.id,
domain_id: domain.id,
credential_id: nil,
raw_headers: kind_of(String),
raw_message: kind_of(String),
bounce: true
)
end
end
context "when there is no return path route" do
it "stores the message normally and resets the state" do
client.handle("DATA")
client.handle("Subject: Bounce: Test")
client.handle("From: #{mail_from}")
client.handle("To: #{rcpt_to}")
client.handle("")
client.handle("This is a test message")
expect(client.handle(".")).to eq "250 OK"
queued_message = QueuedMessage.first
expect(queued_message).to have_attributes(
domain: Postal.config.dns.return_path,
server: server
)
expect(server.message(queued_message.message_id)).to have_attributes(
mail_from: mail_from,
rcpt_to: rcpt_to,
subject: "Bounce: Test",
scope: "incoming",
route_id: nil,
domain_id: nil,
credential_id: nil,
raw_headers: kind_of(String),
raw_message: kind_of(String),
bounce: true
)
end
end
end
context "when receiving an incoming email" do
let(:domain) { create(:domain, owner: server) }
let(:route) { create(:route, server: server, domain: domain) }
let(:credential) { nil }
let(:rcpt_to) { "#{route.name}@#{domain.name}" }
it "stores the message and resets the state" do
client.handle("DATA")
client.handle("Subject: Test")
client.handle("From: #{mail_from}")
client.handle("To: #{rcpt_to}")
client.handle("")
client.handle("This is a test message")
expect(client.handle(".")).to eq "250 OK"
queued_message = QueuedMessage.first
expect(queued_message).to have_attributes(
domain: domain.name,
server: server
)
expect(server.message(queued_message.message_id)).to have_attributes(
mail_from: mail_from,
rcpt_to: rcpt_to,
subject: "Test",
scope: "incoming",
route_id: route.id,
domain_id: domain.id,
credential_id: nil,
raw_headers: kind_of(String),
raw_message: kind_of(String)
)
end
end
end
end
end
end

عرض الملف

@@ -0,0 +1,38 @@
# frozen_string_literal: true
require "rails_helper"
module Postal
module SMTPServer
describe Client do
let(:ip_address) { "1.2.3.4" }
subject(:client) { described_class.new(ip_address) }
describe "HELO" do
it "returns the hostname" do
expect(client.state).to eq :welcome
expect(client.handle("HELO: test.example.com")).to eq "250 #{Postal.config.dns.smtp_server_hostname}"
expect(client.state).to eq :welcomed
end
end
describe "EHLO" do
it "returns the capabilities" do
expect(client.handle("EHLO test.example.com")).to eq ["250-My capabilities are",
"250 AUTH CRAM-MD5 PLAIN LOGIN"]
end
context "when TLS is enabled" do
it "returns capabilities include starttls" do
allow(Postal.config.smtp_server).to receive(:tls_enabled?).and_return(true)
expect(client.handle("EHLO test.example.com")).to eq ["250-My capabilities are",
"250-STARTTLS",
"250 AUTH CRAM-MD5 PLAIN LOGIN"]
end
end
end
end
end
end

عرض الملف

@@ -0,0 +1,35 @@
# frozen_string_literal: true
require "rails_helper"
module Postal
module SMTPServer
describe Client do
let(:ip_address) { "1.2.3.4" }
subject(:client) { described_class.new(ip_address) }
describe "MAIL FROM" do
it "returns an error if no HELO is provided" do
expect(client.handle("MAIL FROM: test@example.com")).to eq "503 EHLO/HELO first please"
expect(client.state).to eq :welcome
end
it "resets the transaction when called" do
expect(client).to receive(:transaction_reset).and_call_original.at_least(3).times
client.handle("HELO test.example.com")
client.handle("MAIL FROM: test@example.com")
client.handle("MAIL FROM: test2@example.com")
end
it "sets the mail from address" do
client.handle("HELO test.example.com")
expect(client.handle("MAIL FROM: test@example.com")).to eq "250 OK"
expect(client.state).to eq :mail_from_received
expect(client.instance_variable_get("@mail_from")).to eq "test@example.com"
end
end
end
end
end

عرض الملف

@@ -0,0 +1,172 @@
# frozen_string_literal: true
require "rails_helper"
module Postal
module SMTPServer
describe Client do
let(:ip_address) { "1.2.3.4" }
subject(:client) { described_class.new(ip_address) }
describe "RCPT TO" do
let(:helo) { "test.example.com" }
let(:mail_from) { "test@example.com" }
before do
client.handle("HELO #{helo}")
client.handle("MAIL FROM: #{mail_from}") if mail_from
end
context "when MAIL FROM has not been sent" do
let(:mail_from) { nil }
it "returns an error if RCPT TO is sent before MAIL FROM" do
expect(client.handle("RCPT TO: no-route-here@internal.com")).to eq "503 EHLO/HELO and MAIL FROM first please"
expect(client.state).to eq :welcomed
end
end
it "returns an error if RCPT TO is not valid" do
expect(client.handle("RCPT TO: blah")).to eq "501 Invalid RCPT TO"
end
it "returns an error if RCPT TO is empty" do
expect(client.handle("RCPT TO: ")).to eq "501 RCPT TO should not be empty"
end
context "when the RCPT TO address is the system return path host" do
it "returns an error if the server does not exist" do
expect(client.handle("RCPT TO: nothing@#{Postal.config.dns.return_path}")).to eq "550 Invalid server token"
end
it "returns an error if the server is suspended" do
server = create(:server, :suspended)
expect(client.handle("RCPT TO: #{server.token}@#{Postal.config.dns.return_path}"))
.to eq "535 Mail server has been suspended"
end
it "adds a recipient if all OK" do
server = create(:server)
address = "#{server.token}@#{Postal.config.dns.return_path}"
expect(client.handle("RCPT TO: #{address}")).to eq "250 OK"
expect(client.recipients).to eq [[:bounce, address, server]]
expect(client.state).to eq :rcpt_to_received
end
end
context "when the RCPT TO address is on a host using the return path prefix" do
it "returns an error if the server does not exist" do
address = "nothing@#{Postal.config.dns.custom_return_path_prefix}.example.com"
expect(client.handle("RCPT TO: #{address}")).to eq "550 Invalid server token"
end
it "returns an error if the server is suspended" do
server = create(:server, :suspended)
address = "#{server.token}@#{Postal.config.dns.custom_return_path_prefix}.example.com"
expect(client.handle("RCPT TO: #{address}")).to eq "535 Mail server has been suspended"
end
it "adds a recipient if all OK" do
server = create(:server)
address = "#{server.token}@#{Postal.config.dns.custom_return_path_prefix}.example.com"
expect(client.handle("RCPT TO: #{address}")).to eq "250 OK"
expect(client.recipients).to eq [[:bounce, address, server]]
expect(client.state).to eq :rcpt_to_received
end
end
context "when the RCPT TO address is within the route domain" do
it "returns an error if the route token is invalid" do
address = "nothing@#{Postal.config.dns.route_domain}"
expect(client.handle("RCPT TO: #{address}")).to eq "550 Invalid route token"
end
it "returns an error if the server is suspended" do
server = create(:server, :suspended)
route = create(:route, server: server)
address = "#{route.token}@#{Postal.config.dns.route_domain}"
expect(client.handle("RCPT TO: #{address}")).to eq "535 Mail server has been suspended"
end
it "returns an error if the route is set to Reject mail" do
server = create(:server)
route = create(:route, server: server, mode: "Reject")
address = "#{route.token}@#{Postal.config.dns.route_domain}"
expect(client.handle("RCPT TO: #{address}")).to eq "550 Route does not accept incoming messages"
end
it "adds a recipient if all OK" do
server = create(:server)
route = create(:route, server: server)
address = "#{route.token}+tag1@#{Postal.config.dns.route_domain}"
expect(client.handle("RCPT TO: #{address}")).to eq "250 OK"
expect(client.recipients).to eq [[:route, "#{route.name}+tag1@#{route.domain.name}", server, { route: route }]]
expect(client.state).to eq :rcpt_to_received
end
end
context "when authenticated and the RCPT TO address is provided" do
it "returns an error if the server is suspended" do
server = create(:server, :suspended)
credential = create(:credential, server: server, type: "SMTP")
expect(client.handle("AUTH PLAIN #{credential.to_smtp_plain}")).to match(/235 Granted for /)
expect(client.handle("RCPT TO: outgoing@example.com")).to eq "535 Mail server has been suspended"
end
it "adds a recipient if all OK" do
server = create(:server)
credential = create(:credential, server: server, type: "SMTP")
expect(client.handle("AUTH PLAIN #{credential.to_smtp_plain}")).to match(/235 Granted for /)
expect(client.handle("RCPT TO: outgoing@example.com")).to eq "250 OK"
expect(client.recipients).to eq [[:credential, "outgoing@example.com", server]]
expect(client.state).to eq :rcpt_to_received
end
end
context "when not authenticated and the RCPT TO address is a route" do
it "returns an error if the server is suspended" do
server = create(:server, :suspended)
route = create(:route, server: server)
address = "#{route.name}@#{route.domain.name}"
expect(client.handle("RCPT TO: #{address}")).to eq "535 Mail server has been suspended"
end
it "returns an error if the route is set to Reject mail" do
server = create(:server)
route = create(:route, server: server, mode: "Reject")
address = "#{route.name}@#{route.domain.name}"
expect(client.handle("RCPT TO: #{address}")).to eq "550 Route does not accept incoming messages"
end
it "adds a recipient if all OK" do
server = create(:server)
route = create(:route, server: server)
address = "#{route.name}@#{route.domain.name}"
expect(client.handle("RCPT TO: #{address}")).to eq "250 OK"
expect(client.recipients).to eq [[:route, address, server, { route: route }]]
expect(client.state).to eq :rcpt_to_received
end
end
context "when not authenticated and RCPT TO does not match a route" do
it "returns an error" do
expect(client.handle("RCPT TO: nothing@nothing.com")).to eq "530 Authentication required"
end
context "when the connecting IP has an credential" do
it "adds a recipient" do
server = create(:server)
create(:credential, server: server, type: "SMTP-IP", key: "1.0.0.0/8")
address = "test@example.com"
expect(client.handle("RCPT TO: #{address}")).to eq "250 OK"
expect(client.recipients).to eq [[:credential, address, server]]
expect(client.state).to eq :rcpt_to_received
end
end
end
end
end
end
end

عرض الملف

@@ -0,0 +1,14 @@
# frozen_string_literal: true
require "rails_helper"
module Postal
module SMTPServer
describe Client do
let(:ip_address) { "1.2.3.4" }
subject(:client) { described_class.new(ip_address) }
end
end
end

عرض الملف

@@ -6,6 +6,7 @@ require File.expand_path("../config/environment", __dir__)
require "rspec/rails"
require "spec_helper"
require "factory_bot"
require "timecop"
require "database_cleaner"
DatabaseCleaner.allow_remote_database_url = true