مراية لـ
https://github.com/postalserver/postal.git
تم المزامنة 2025-12-01 05:43:04 +00:00
refactor: move lib/postal/smtp_server to app/lib/smtp_server
هذا الالتزام موجود في:
@@ -1,122 +0,0 @@
|
||||
# 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
|
||||
@@ -1,89 +0,0 @@
|
||||
# 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}")
|
||||
end
|
||||
|
||||
it "logs headers" do
|
||||
client.handle("DATA")
|
||||
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
|
||||
Timecop.freeze do
|
||||
client.handle("DATA")
|
||||
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
|
||||
end
|
||||
@@ -1,208 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
module Postal
|
||||
module SMTPServer
|
||||
|
||||
describe Client do
|
||||
let(:ip_address) { "1.2.3.4" }
|
||||
let(:server) { create(:server) }
|
||||
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
|
||||
|
||||
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
|
||||
@@ -1,38 +0,0 @@
|
||||
# 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
|
||||
@@ -1,35 +0,0 @@
|
||||
# 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
|
||||
@@ -1,172 +0,0 @@
|
||||
# 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
|
||||
@@ -1,14 +0,0 @@
|
||||
# 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
|
||||
120
spec/lib/smtp_server/client/auth_spec.rb
Normal file
120
spec/lib/smtp_server/client/auth_spec.rb
Normal file
@@ -0,0 +1,120 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
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
|
||||
87
spec/lib/smtp_server/client/data_spec.rb
Normal file
87
spec/lib/smtp_server/client/data_spec.rb
Normal file
@@ -0,0 +1,87 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
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}")
|
||||
end
|
||||
|
||||
it "logs headers" do
|
||||
client.handle("DATA")
|
||||
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
|
||||
Timecop.freeze do
|
||||
client.handle("DATA")
|
||||
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
|
||||
206
spec/lib/smtp_server/client/finished_spec.rb
Normal file
206
spec/lib/smtp_server/client/finished_spec.rb
Normal file
@@ -0,0 +1,206 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
module SMTPServer
|
||||
|
||||
describe Client do
|
||||
let(:ip_address) { "1.2.3.4" }
|
||||
let(:server) { create(:server) }
|
||||
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
|
||||
|
||||
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
|
||||
36
spec/lib/smtp_server/client/helo_spec.rb
Normal file
36
spec/lib/smtp_server/client/helo_spec.rb
Normal file
@@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
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
|
||||
33
spec/lib/smtp_server/client/mail_from_spec.rb
Normal file
33
spec/lib/smtp_server/client/mail_from_spec.rb
Normal file
@@ -0,0 +1,33 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
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
|
||||
170
spec/lib/smtp_server/client/rcpt_to_spec.rb
Normal file
170
spec/lib/smtp_server/client/rcpt_to_spec.rb
Normal file
@@ -0,0 +1,170 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
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
|
||||
12
spec/lib/smtp_server/client_spec.rb
Normal file
12
spec/lib/smtp_server/client_spec.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
module SMTPServer
|
||||
|
||||
describe Client do
|
||||
let(:ip_address) { "1.2.3.4" }
|
||||
subject(:client) { described_class.new(ip_address) }
|
||||
end
|
||||
|
||||
end
|
||||
المرجع في مشكلة جديدة
حظر مستخدم