1
0
مراية لـ https://github.com/postalserver/postal.git تم المزامنة 2025-12-01 05:43:04 +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 # Always use double quotes
Style/StringLiterals: Style/StringLiterals:
EnforcedStyle: double_quotes EnforcedStyle: double_quotes
AutoCorrect: true
# We prefer arrays of symbols to look like an array of symbols. # We prefer arrays of symbols to look like an array of symbols.
# For example: [:one, :two, :three] as opposed to %i[one two three] # For example: [:one, :two, :three] as opposed to %i[one two three]

عرض الملف

@@ -48,9 +48,10 @@ end
group :development do group :development do
gem "annotate" gem "annotate"
gem "database_cleaner", require: false 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", require: false
gem "rspec-rails", require: false gem "rspec-rails", require: false
gem "rubocop" gem "rubocop"
gem "rubocop-rails" gem "rubocop-rails"
gem "timecop"
end end

عرض الملف

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

عرض الملف

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

عرض الملف

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

عرض الملف

@@ -11,6 +11,12 @@ module Postal
LOG_REDACTION_STRING = "[redacted]" LOG_REDACTION_STRING = "[redacted]"
attr_reader :logging_enabled 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) def initialize(ip_address)
@logging_enabled = true @logging_enabled = true
@@ -55,19 +61,6 @@ module Postal
end end
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? def finished?
@finished || false @finished || false
end end
@@ -137,7 +130,11 @@ module Postal
@helo_name = data.strip.split(" ", 2)[1] @helo_name = data.strip.split(" ", 2)[1]
transaction_reset transaction_reset
@state = :welcomed @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 end
def helo(data) def helo(data)
@@ -194,7 +191,7 @@ module Postal
"334 UGFzc3dvcmQ6" # "Password:" "334 UGFzc3dvcmQ6" # "Password:"
end end
data = data.gsub!(/AUTH LOGIN ?/i, "") data = data.gsub(/AUTH LOGIN ?/i, "")
if data.strip == "" if data.strip == ""
@proc = username_handler @proc = username_handler
"334 VXNlcm5hbWU6" # "Username:" "334 VXNlcm5hbWU6" # "Username:"
@@ -226,6 +223,7 @@ module Postal
log "\e[33m WARN: AUTH failure for #{@ip_address}\e[0m" log "\e[33m WARN: AUTH failure for #{@ip_address}\e[0m"
next "535 Denied" next "535 Denied"
end end
grant = nil grant = nil
server.credentials.where(type: "SMTP").each do |credential| server.credentials.where(type: "SMTP").each do |credential|
correct_response = OpenSSL::HMAC.hexdigest(CRAM_MD5_DIGEST, credential.key, challenge) 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}" grant = "235 Granted for #{credential.server.organization.permalink}/#{credential.server.permalink}"
break break
end end
if grant.nil? if grant.nil?
log "\e[33m WARN: AUTH failure for #{@ip_address}\e[0m" log "\e[33m WARN: AUTH failure for #{@ip_address}\e[0m"
next "535 Denied" next "535 Denied"
end end
grant grant
end end
@@ -458,6 +458,7 @@ module Postal
msg.mail_from = @mail_from msg.mail_from = @mail_from
msg.raw_message = @data msg.raw_message = @data
msg.received_with_ssl = @tls msg.received_with_ssl = @tls
msg.bounce = 1
end end
else else
# There's no return path route, we just need to insert the mesage # There's no return path route, we just need to insert the mesage
@@ -489,6 +490,19 @@ module Postal
states.include?(@state) states.include?(@state)
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
end end
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 FactoryBot.define do
factory :domain do factory :domain do
association :owner, factory: :user association :owner, factory: :organization
sequence(:name) { |n| "example#{n}.com" } sequence(:name) { |n| "example#{n}.com" }
verification_method { "DNS" } verification_method { "DNS" }
verified_at { Time.now } 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" } mode { "Live" }
provision_database { false } provision_database { false }
sequence(:permalink) { |n| "server#{n}" } sequence(:permalink) { |n| "server#{n}" }
trait :suspended do
suspended_at { Time.current }
end
end 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 "rspec/rails"
require "spec_helper" require "spec_helper"
require "factory_bot" require "factory_bot"
require "timecop"
require "database_cleaner" require "database_cleaner"
DatabaseCleaner.allow_remote_database_url = true DatabaseCleaner.allow_remote_database_url = true