مراية لـ
https://github.com/postalserver/postal.git
تم المزامنة 2025-12-01 05:43:04 +00:00
497 أسطر
17 KiB
Ruby
497 أسطر
17 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "rails_helper"
|
|
|
|
RSpec.describe SMTPSender do
|
|
subject(:sender) { described_class.new("example.com") }
|
|
|
|
let(:smtp_start_error) { nil }
|
|
let(:smtp_send_message_error) { nil }
|
|
let(:smtp_send_message_result) { double("Result", string: "accepted") }
|
|
|
|
before do
|
|
# Mock the SMTP client endpoint so that we can avoid making any actual
|
|
# SMTP connections but still mock things as appropriate.
|
|
allow(SMTPClient::Endpoint).to receive(:new).and_wrap_original do |original, *args, **kwargs|
|
|
endpoint = original.call(*args, **kwargs)
|
|
|
|
allow(endpoint).to receive(:start_smtp_session) do |**ikwargs|
|
|
if error = smtp_start_error&.call(endpoint, ikwargs[:allow_ssl])
|
|
raise error
|
|
end
|
|
end
|
|
|
|
allow(endpoint).to receive(:send_message) do |message|
|
|
if error = smtp_send_message_error&.call(endpoint, message)
|
|
raise error
|
|
end
|
|
|
|
smtp_send_message_result
|
|
end
|
|
allow(endpoint).to receive(:finish_smtp_session)
|
|
allow(endpoint).to receive(:reset_smtp_session)
|
|
allow(endpoint).to receive(:smtp_client) do
|
|
Net::SMTP.new(endpoint.ip_address, endpoint.server.port)
|
|
end
|
|
endpoint
|
|
end
|
|
end
|
|
|
|
before do
|
|
# Override the DNS resolver to return empty arrays by default for A and AAAA
|
|
# DNS lookups to avoid making requests to public servers.
|
|
allow(DNSResolver.local).to receive(:aaaa).and_return([])
|
|
allow(DNSResolver.local).to receive(:a).and_return([])
|
|
end
|
|
|
|
describe "#start" do
|
|
context "when no servers are provided to the class and there are no SMTP relays" do
|
|
before do
|
|
allow(DNSResolver.local).to receive(:mx).and_return([[5, "mx1.example.com"], [10, "mx2.example.com"]])
|
|
allow(DNSResolver.local).to receive(:a).with("mx1.example.com").and_return(["1.2.3.4"])
|
|
allow(DNSResolver.local).to receive(:a).with("mx2.example.com").and_return(["6.7.8.9"])
|
|
end
|
|
|
|
it "attempts to create an SMTP connection for each endpoint for each MX server for them" do
|
|
endpoint = sender.start
|
|
expect(endpoint).to be_a SMTPClient::Endpoint
|
|
expect(endpoint).to have_attributes(
|
|
ip_address: "1.2.3.4",
|
|
server: have_attributes(hostname: "mx1.example.com", port: 25, ssl_mode: SMTPClient::SSLModes::AUTO)
|
|
)
|
|
end
|
|
end
|
|
|
|
context "when there are no servers provided to the class but there are SMTP relays" do
|
|
before do
|
|
allow(SMTPSender).to receive(:smtp_relays).and_return([SMTPClient::Server.new("relay.example.com", port: 2525, ssl_mode: SMTPClient::SSLModes::TLS)])
|
|
allow(DNSResolver.local).to receive(:a).with("relay.example.com").and_return(["1.2.3.4"])
|
|
end
|
|
|
|
it "attempts to use the relays" do
|
|
endpoint = sender.start
|
|
expect(endpoint).to be_a SMTPClient::Endpoint
|
|
expect(endpoint).to have_attributes(
|
|
ip_address: "1.2.3.4",
|
|
server: have_attributes(hostname: "relay.example.com", port: 2525, ssl_mode: SMTPClient::SSLModes::TLS)
|
|
)
|
|
end
|
|
end
|
|
|
|
context "when there are servers provided to the class" do
|
|
let(:server) { SMTPClient::Server.new("custom.example.com") }
|
|
|
|
subject(:sender) { described_class.new("example.com", servers: [server]) }
|
|
|
|
before do
|
|
allow(DNSResolver.local).to receive(:a).with("custom.example.com").and_return(["1.2.3.4"])
|
|
end
|
|
|
|
it "uses the provided servers" do
|
|
endpoint = sender.start
|
|
expect(endpoint).to be_a SMTPClient::Endpoint
|
|
expect(endpoint).to have_attributes(
|
|
ip_address: "1.2.3.4",
|
|
server: server
|
|
)
|
|
end
|
|
end
|
|
|
|
context "when a source IP is given without IPv6 and an endpoint is IPv6 enabled" do
|
|
let(:source_ip_address) { create(:ip_address, ipv6: nil) }
|
|
let(:server) { SMTPClient::Server.new("custom.example.com") }
|
|
subject(:sender) { described_class.new("example.com", source_ip_address, servers: [server]) }
|
|
|
|
before do
|
|
allow(DNSResolver.local).to receive(:aaaa).with("custom.example.com").and_return(["2a00:67a0:a::1"])
|
|
allow(DNSResolver.local).to receive(:a).with("custom.example.com").and_return(["1.2.3.4"])
|
|
end
|
|
|
|
it "returns the IPv4 version" do
|
|
endpoint = sender.start
|
|
expect(endpoint).to be_a SMTPClient::Endpoint
|
|
expect(endpoint).to have_attributes(
|
|
ip_address: "1.2.3.4",
|
|
server: server
|
|
)
|
|
end
|
|
end
|
|
|
|
context "when there are no servers to connect to" do
|
|
it "returns false" do
|
|
expect(sender.start).to be false
|
|
end
|
|
end
|
|
|
|
context "when the first server tried cannot be connected to" do
|
|
let(:server1) { SMTPClient::Server.new("custom1.example.com") }
|
|
let(:server2) { SMTPClient::Server.new("custom2.example.com") }
|
|
|
|
let(:smtp_start_error) do
|
|
proc do |endpoint|
|
|
Errno::ECONNREFUSED if endpoint.ip_address == "1.2.3.4"
|
|
end
|
|
end
|
|
|
|
before do
|
|
allow(DNSResolver.local).to receive(:a).with("custom1.example.com").and_return(["1.2.3.4"])
|
|
allow(DNSResolver.local).to receive(:a).with("custom2.example.com").and_return(["2.3.4.5"])
|
|
end
|
|
|
|
subject(:sender) { described_class.new("example.com", servers: [server1, server2]) }
|
|
|
|
it "tries the second" do
|
|
endpoint = sender.start
|
|
expect(endpoint).to be_a SMTPClient::Endpoint
|
|
expect(endpoint).to have_attributes(
|
|
ip_address: "2.3.4.5",
|
|
server: have_attributes(hostname: "custom2.example.com")
|
|
)
|
|
end
|
|
|
|
it "includes both endpoints in the array of endpoints tried" do
|
|
sender.start
|
|
expect(sender.endpoints).to match([have_attributes(ip_address: "1.2.3.4"),
|
|
have_attributes(ip_address: "2.3.4.5")])
|
|
end
|
|
end
|
|
|
|
context "when the server returns an SSL error and SSL mode is Auto" do
|
|
let(:server) { SMTPClient::Server.new("custom.example.com") }
|
|
|
|
let(:smtp_start_error) do
|
|
proc do |endpoint, allow_ssl|
|
|
OpenSSL::SSL::SSLError if allow_ssl && endpoint.server.ssl_mode == "Auto"
|
|
end
|
|
end
|
|
|
|
before do
|
|
allow(DNSResolver.local).to receive(:aaaa).with("custom.example.com").and_return([])
|
|
allow(DNSResolver.local).to receive(:a).with("custom.example.com").and_return(["1.2.3.4"])
|
|
end
|
|
|
|
subject(:sender) { described_class.new("example.com", servers: [server]) }
|
|
|
|
it "attempts to reconnect without SSL" do
|
|
endpoint = sender.start
|
|
expect(endpoint).to be_a SMTPClient::Endpoint
|
|
expect(endpoint).to have_attributes(ip_address: "1.2.3.4")
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#send_message" do
|
|
let(:server) { create(:server) }
|
|
let(:domain) { create(:domain, server: server) }
|
|
let(:dns_result) { [] }
|
|
let(:message) { MessageFactory.outgoing(server, domain: domain) }
|
|
|
|
let(:smtp_client_server) { SMTPClient::Server.new("mx1.example.com") }
|
|
subject(:sender) { described_class.new("example.com", servers: [smtp_client_server]) }
|
|
|
|
before do
|
|
allow(DNSResolver.local).to receive(:a).with("mx1.example.com").and_return(dns_result)
|
|
sender.start
|
|
end
|
|
|
|
context "when there is no current endpoint to use" do
|
|
it "returns a SoftFail" do
|
|
result = sender.send_message(message)
|
|
expect(result).to be_a SendResult
|
|
expect(result).to have_attributes(
|
|
type: "SoftFail",
|
|
retry: true,
|
|
output: "",
|
|
details: /No SMTP servers were available for example.com. No hosts to try./,
|
|
connect_error: true
|
|
)
|
|
end
|
|
end
|
|
|
|
context "when there is an endpoint" do
|
|
let(:dns_result) { ["1.2.3.4"] }
|
|
|
|
context "it sends the message to the endpoint" do
|
|
context "if the message is a bounce" do
|
|
let(:message) { MessageFactory.outgoing(server, domain: domain) { |m| m.bounce = true } }
|
|
|
|
it "sends an empty MAIL FROM" do
|
|
sender.send_message(message)
|
|
expect(sender.endpoints.last).to have_received(:send_message).with(
|
|
kind_of(String),
|
|
"",
|
|
["john@example.com"]
|
|
)
|
|
end
|
|
end
|
|
|
|
context "if the domain has a valid custom return path" do
|
|
let(:domain) { create(:domain, return_path_status: "OK") }
|
|
|
|
it "sends the custom return path as MAIL FROM" do
|
|
sender.send_message(message)
|
|
expect(sender.endpoints.last).to have_received(:send_message).with(
|
|
kind_of(String),
|
|
"#{server.token}@#{domain.return_path_domain}",
|
|
["john@example.com"]
|
|
)
|
|
end
|
|
end
|
|
|
|
context "if the domain has no valid custom return path" do
|
|
it "sends the server default return path as MAIL FROM" do
|
|
sender.send_message(message)
|
|
expect(sender.endpoints.last).to have_received(:send_message).with(
|
|
kind_of(String),
|
|
"#{server.token}@#{Postal::Config.dns.return_path_domain}",
|
|
["john@example.com"]
|
|
)
|
|
end
|
|
end
|
|
|
|
context "if the sender has specified an RCPT TO" do
|
|
subject(:sender) { described_class.new("example.com", servers: [smtp_client_server], rcpt_to: "custom@example.com") }
|
|
|
|
it "sends the specified RCPT TO" do
|
|
sender.send_message(message)
|
|
expect(sender.endpoints.last).to have_received(:send_message).with(
|
|
kind_of(String),
|
|
kind_of(String),
|
|
["custom@example.com"]
|
|
)
|
|
end
|
|
end
|
|
|
|
context "if the sender has not specified an RCPT TO" do
|
|
it "uses the RCPT TO from the message" do
|
|
sender.send_message(message)
|
|
expect(sender.endpoints.last).to have_received(:send_message).with(
|
|
kind_of(String),
|
|
kind_of(String),
|
|
["john@example.com"]
|
|
)
|
|
end
|
|
end
|
|
|
|
context "if the configuration says to add the Resent-Sender header" do
|
|
it "adds the resent-sender header" do
|
|
sender.send_message(message)
|
|
expect(sender.endpoints.last).to have_received(:send_message).with(
|
|
"Resent-Sender: #{server.token}@#{Postal::Config.dns.return_path_domain}\r\n#{message.raw_message}",
|
|
kind_of(String),
|
|
kind_of(Array)
|
|
)
|
|
end
|
|
end
|
|
|
|
context "if the configuration says to not add the Resent-From header" do
|
|
before do
|
|
allow(Postal::Config.postal).to receive(:use_resent_sender_header?).and_return(false)
|
|
end
|
|
|
|
it "does not add the resent-from header" do
|
|
sender.send_message(message)
|
|
expect(sender.endpoints.last).to have_received(:send_message).with(
|
|
message.raw_message,
|
|
kind_of(String),
|
|
kind_of(Array)
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when the message is accepted" do
|
|
it "returns a Sent result" do
|
|
result = sender.send_message(message)
|
|
expect(result).to be_a SendResult
|
|
expect(result).to have_attributes(
|
|
type: "Sent",
|
|
details: "Message for john@example.com accepted by 1.2.3.4:25 (mx1.example.com)",
|
|
output: "accepted"
|
|
)
|
|
end
|
|
end
|
|
|
|
context "when SMTP server is busy" do
|
|
let(:smtp_send_message_error) { proc { Net::SMTPServerBusy.new("SMTP server was busy") } }
|
|
|
|
it "returns a SoftFail" do
|
|
result = sender.send_message(message)
|
|
expect(result).to be_a SendResult
|
|
expect(result).to have_attributes(
|
|
type: "SoftFail",
|
|
retry: true,
|
|
details: /Temporary SMTP delivery error when sending/
|
|
)
|
|
end
|
|
|
|
it "resets the endpoint SMTP sesssion" do
|
|
sender.send_message(message)
|
|
expect(sender.endpoints.last).to have_received(:reset_smtp_session)
|
|
end
|
|
end
|
|
|
|
context "when the SMTP server returns an error if a retry time in seconds" do
|
|
let(:smtp_send_message_error) { proc { Net::SMTPServerBusy.new("Try again in 30 seconds") } }
|
|
|
|
it "returns a SoftFail with the retry time from the error" do
|
|
result = sender.send_message(message)
|
|
expect(result).to be_a SendResult
|
|
expect(result).to have_attributes(
|
|
type: "SoftFail",
|
|
retry: 40
|
|
)
|
|
end
|
|
end
|
|
|
|
context "when the SMTP server returns an error if a retry time in minutes" do
|
|
let(:smtp_send_message_error) { proc { Net::SMTPServerBusy.new("Try again in 5 minutes") } }
|
|
|
|
it "returns a SoftFail with the retry time from the error" do
|
|
result = sender.send_message(message)
|
|
expect(result).to be_a SendResult
|
|
expect(result).to have_attributes(
|
|
type: "SoftFail",
|
|
retry: 310
|
|
)
|
|
end
|
|
end
|
|
|
|
context "when there is an SMTP authentication error" do
|
|
let(:smtp_send_message_error) { proc { Net::SMTPAuthenticationError.new("Denied") } }
|
|
|
|
it "returns a SoftFail" do
|
|
result = sender.send_message(message)
|
|
expect(result).to be_a SendResult
|
|
expect(result).to have_attributes(
|
|
type: "SoftFail",
|
|
details: /Temporary SMTP delivery error when sending/
|
|
)
|
|
end
|
|
|
|
it "resets the endpoint SMTP sesssion" do
|
|
sender.send_message(message)
|
|
expect(sender.endpoints.last).to have_received(:reset_smtp_session)
|
|
end
|
|
end
|
|
|
|
context "when there is a timeout" do
|
|
let(:smtp_send_message_error) { proc { Net::ReadTimeout.new } }
|
|
|
|
it "returns a SoftFail" do
|
|
result = sender.send_message(message)
|
|
expect(result).to be_a SendResult
|
|
expect(result).to have_attributes(
|
|
type: "SoftFail",
|
|
details: /Temporary SMTP delivery error when sending/
|
|
)
|
|
end
|
|
|
|
it "resets the endpoint SMTP sesssion" do
|
|
sender.send_message(message)
|
|
expect(sender.endpoints.last).to have_received(:reset_smtp_session)
|
|
end
|
|
end
|
|
|
|
context "when there is an SMTP syntax error" do
|
|
let(:smtp_send_message_error) { proc { Net::SMTPSyntaxError.new("Syntax error") } }
|
|
|
|
it "returns a SoftFail" do
|
|
result = sender.send_message(message)
|
|
expect(result).to be_a SendResult
|
|
expect(result).to have_attributes(
|
|
type: "SoftFail",
|
|
output: "Syntax error",
|
|
details: /Temporary SMTP delivery error when sending/
|
|
)
|
|
end
|
|
|
|
it "resets the endpoint SMTP sesssion" do
|
|
sender.send_message(message)
|
|
expect(sender.endpoints.last).to have_received(:reset_smtp_session)
|
|
end
|
|
end
|
|
|
|
context "when there is an unknown SMTP error" do
|
|
let(:smtp_send_message_error) { proc { Net::SMTPUnknownError.new("unknown error") } }
|
|
|
|
it "returns a SoftFail" do
|
|
result = sender.send_message(message)
|
|
expect(result).to be_a SendResult
|
|
expect(result).to have_attributes(
|
|
type: "SoftFail",
|
|
output: "unknown error",
|
|
details: /Temporary SMTP delivery error when sending/
|
|
)
|
|
end
|
|
|
|
it "resets the endpoint SMTP sesssion" do
|
|
sender.send_message(message)
|
|
expect(sender.endpoints.last).to have_received(:reset_smtp_session)
|
|
end
|
|
end
|
|
|
|
context "when there is an fatal SMTP error" do
|
|
let(:smtp_send_message_error) { proc { Net::SMTPFatalError.new("fatal error") } }
|
|
|
|
it "returns a HardFail" do
|
|
result = sender.send_message(message)
|
|
expect(result).to be_a SendResult
|
|
expect(result).to have_attributes(
|
|
type: "HardFail",
|
|
output: "fatal error",
|
|
details: /Permanent SMTP delivery error when sending/
|
|
)
|
|
end
|
|
|
|
it "resets the endpoint SMTP sesssion" do
|
|
sender.send_message(message)
|
|
expect(sender.endpoints.last).to have_received(:reset_smtp_session)
|
|
end
|
|
end
|
|
|
|
context "when there is an unexpected error" do
|
|
let(:smtp_send_message_error) { proc { ZeroDivisionError.new("divided by 0") } }
|
|
|
|
it "returns a SoftFail" do
|
|
result = sender.send_message(message)
|
|
expect(result).to be_a SendResult
|
|
expect(result).to have_attributes(
|
|
type: "SoftFail",
|
|
output: "divided by 0",
|
|
details: /An error occurred while sending the message/
|
|
)
|
|
end
|
|
|
|
it "resets the endpoint SMTP sesssion" do
|
|
sender.send_message(message)
|
|
expect(sender.endpoints.last).to have_received(:reset_smtp_session)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#finish" do
|
|
let(:server) { SMTPClient::Server.new("custom.example.com") }
|
|
|
|
subject(:sender) { described_class.new("example.com", servers: [server]) }
|
|
|
|
let(:smtp_start_error) do
|
|
proc do |endpoint|
|
|
Errno::ECONNREFUSED if endpoint.ip_address == "1.2.3.4"
|
|
end
|
|
end
|
|
|
|
before do
|
|
allow(DNSResolver.local).to receive(:a).with("custom.example.com").and_return(["1.2.3.4", "2.3.4.5"])
|
|
sender.start
|
|
end
|
|
|
|
it "calls finish_smtp_session on all endpoints" do
|
|
sender.finish
|
|
expect(sender.endpoints.size).to eq 2
|
|
expect(sender.endpoints).to all have_received(:finish_smtp_session).at_least(:once)
|
|
end
|
|
end
|
|
end
|