مراية لـ
https://github.com/postalserver/postal.git
تم المزامنة 2025-12-01 05:43:04 +00:00
test: add initial tests for Postal::SMTPServer::Client
هذا الالتزام موجود في:
@@ -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]
|
||||||
|
|||||||
3
Gemfile
3
Gemfile
@@ -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
|
||||||
|
|||||||
52
Gemfile.lock
52
Gemfile.lock
@@ -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
|
||||||
|
|||||||
9
spec/factories/credential_factory.rb
Normal file
9
spec/factories/credential_factory.rb
Normal file
@@ -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 }
|
||||||
|
|||||||
11
spec/factories/http_endpoint_factory.rb
Normal file
11
spec/factories/http_endpoint_factory.rb
Normal file
@@ -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
|
||||||
14
spec/factories/route_factory.rb
Normal file
14
spec/factories/route_factory.rb
Normal file
@@ -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
|
||||||
|
|||||||
122
spec/lib/postal/smtp_server/client/auth_spec.rb
Normal file
122
spec/lib/postal/smtp_server/client/auth_spec.rb
Normal file
@@ -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
|
||||||
86
spec/lib/postal/smtp_server/client/data_spec.rb
Normal file
86
spec/lib/postal/smtp_server/client/data_spec.rb
Normal file
@@ -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
|
||||||
212
spec/lib/postal/smtp_server/client/finished_spec.rb
Normal file
212
spec/lib/postal/smtp_server/client/finished_spec.rb
Normal file
@@ -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
|
||||||
38
spec/lib/postal/smtp_server/client/helo_spec.rb
Normal file
38
spec/lib/postal/smtp_server/client/helo_spec.rb
Normal file
@@ -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
|
||||||
35
spec/lib/postal/smtp_server/client/mail_from_spec.rb
Normal file
35
spec/lib/postal/smtp_server/client/mail_from_spec.rb
Normal file
@@ -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
|
||||||
172
spec/lib/postal/smtp_server/client/rcpt_to_spec.rb
Normal file
172
spec/lib/postal/smtp_server/client/rcpt_to_spec.rb
Normal file
@@ -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
|
||||||
14
spec/lib/postal/smtp_server/client_spec.rb
Normal file
14
spec/lib/postal/smtp_server/client_spec.rb
Normal file
@@ -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
|
||||||
|
|||||||
المرجع في مشكلة جديدة
حظر مستخدم