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 حذوفات

عرض الملف

@@ -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