Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion app/lib/smtp_client/endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,12 @@ def start_smtp_session(source_ip_address: nil, allow_ssl: true)
@smtp_client.disable_tls
end

@smtp_client.start(@source_ip_address ? @source_ip_address.hostname : self.class.default_helo_hostname)
helo_hostname = @source_ip_address ? @source_ip_address.hostname : self.class.default_helo_hostname
if @server.username.present?
@smtp_client.start(helo_hostname, @server.username, @server.password, :login)
else
@smtp_client.start(helo_hostname)
end

@smtp_client
end
Expand Down
6 changes: 5 additions & 1 deletion app/lib/smtp_client/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ class Server

attr_reader :hostname
attr_reader :port
attr_reader :username
attr_reader :password
attr_accessor :ssl_mode

def initialize(hostname, port: 25, ssl_mode: SSLModes::AUTO)
def initialize(hostname, port: 25, ssl_mode: SSLModes::AUTO, username: nil, password: nil)
@hostname = hostname
@port = port
@ssl_mode = ssl_mode
@username = username
@password = password
end

# Return all IP addresses for this server by resolving its hostname.
Expand Down
6 changes: 5 additions & 1 deletion app/senders/smtp_sender.rb
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,11 @@ def smtp_relays
relays = relays.filter_map do |relay|
next unless relay.host.present?

SMTPClient::Server.new(relay.host, port: relay.port, ssl_mode: relay.ssl_mode)
SMTPClient::Server.new(relay.host,
port: relay.port,
ssl_mode: relay.ssl_mode,
username: relay.username,
password: relay.password)
end

@smtp_relays = relays.empty? ? nil : relays
Expand Down
2 changes: 1 addition & 1 deletion doc/config/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ This document contains all the environment variables which are available for thi
| `POSTAL_USE_LOCAL_NS_FOR_DOMAIN_VERIFICATION` | Boolean | Domain verification and checking usually checks with a domain's nameserver. Enable this to check with the server's local nameservers. | false |
| `POSTAL_USE_RESENT_SENDER_HEADER` | Boolean | Append a Resend-Sender header to all outgoing e-mails | true |
| `POSTAL_SIGNING_KEY_PATH` | String | Path to the private key used for signing | $config-file-root/signing.key |
| `POSTAL_SMTP_RELAYS` | Array of strings | An array of SMTP relays in the format of smtp://host:port | [] |
| `POSTAL_SMTP_RELAYS` | Array of strings | An array of SMTP relays in the format of smtp://host:port or smtp://username:password@host:port | [] |
| `POSTAL_TRUSTED_PROXIES` | Array of strings | An array of IP addresses to trust for proxying requests to Postal (in addition to localhost addresses) | [] |
| `POSTAL_QUEUED_MESSAGE_LOCK_STALE_DAYS` | Integer | The number of days after which to consider a lock as stale. Messages with stale locks will be removed and not retried. | 1 |
| `POSTAL_BATCH_QUEUED_MESSAGES` | Boolean | When enabled queued messages will be de-queued in batches based on their destination | true |
Expand Down
2 changes: 1 addition & 1 deletion doc/config/yaml.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ postal:
use_resent_sender_header: true
# Path to the private key used for signing
signing_key_path: $config-file-root/signing.key
# An array of SMTP relays in the format of smtp://host:port
# An array of SMTP relays in the format of smtp://host:port or smtp://username:password@host:port
smtp_relays: []
# An array of IP addresses to trust for proxying requests to Postal (in addition to localhost addresses)
trusted_proxies: []
Expand Down
7 changes: 5 additions & 2 deletions lib/postal/config_schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,18 @@ module Postal

string :smtp_relays do
array
description "An array of SMTP relays in the format of smtp://host:port"
description "An array of SMTP relays in the format of smtp://host:port or smtp://username:password@host:port"
transform do |value|
uri = URI.parse(value)
query = uri.query ? CGI.parse(uri.query) : {}
{
relay = {
host: uri.host,
port: uri.port || 25,
ssl_mode: query["ssl_mode"]&.first || "Auto"
}
relay[:username] = CGI.unescape(uri.user) if uri.user
relay[:password] = CGI.unescape(uri.password) if uri.password
relay
end
end

Expand Down
68 changes: 68 additions & 0 deletions spec/lib/postal/config_schema_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# frozen_string_literal: true

require "rails_helper"
require "konfig/error"
require "konfig/sources/abstract"

module Postal
RSpec.describe ConfigSchema do
let(:schema) { described_class }

class TestConfigSource < Konfig::Sources::Abstract
def initialize(config)
super()
@config = config
end

def get(path, attribute: nil)
value = path.reduce(@config) do |memo, key|
next nil unless memo.is_a?(Hash)

memo[key] || memo[key.to_s] || memo[key.to_sym]
end
raise Konfig::ValueNotPresentError if value.nil?

value
end
end

def build_config(smtp_relays)
Konfig::Config.build(
schema,
sources: [
TestConfigSource.new(
"postal" => {
"smtp_relays" => smtp_relays
}
)
]
)
end

describe "postal.smtp_relays" do
it "parses relay URLs without credentials" do
config = build_config(["smtp://relay.example.com:587?ssl_mode=TLS"])

expect(config.postal.smtp_relays).to eq [
{ "host" => "relay.example.com", "port" => 587, "ssl_mode" => "TLS" },
]
end

it "parses relay URLs with credentials" do
config = build_config(["smtp://relay-user:relay-pass@relay.example.com:587?ssl_mode=TLS"])

expect(config.postal.smtp_relays).to eq [
{ "host" => "relay.example.com", "port" => 587, "ssl_mode" => "TLS", "username" => "relay-user", "password" => "relay-pass" },
]
end

it "decodes percent-encoded relay credentials" do
config = build_config(["smtp://relay%40user:pa%24%24%3Aword@relay.example.com:587?ssl_mode=TLS"])

expect(config.postal.smtp_relays).to eq [
{ "host" => "relay.example.com", "port" => 587, "ssl_mode" => "TLS", "username" => "relay@user", "password" => "pa$$:word" },
]
end
end
end
end
24 changes: 23 additions & 1 deletion spec/lib/smtp_client/endpoint_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ module SMTPClient

RSpec.describe Endpoint do
let(:ssl_mode) { SSLModes::AUTO }
let(:server) { Server.new("mx1.example.com", port: 25, ssl_mode: ssl_mode) }
let(:username) { nil }
let(:password) { nil }
let(:server) { Server.new("mx1.example.com", port: 25, ssl_mode: ssl_mode, username: username, password: password) }
let(:ip) { "1.2.3.4" }

before do
Expand Down Expand Up @@ -88,6 +90,16 @@ module SMTPClient
expect(endpoint.smtp_client).to have_received(:start).with(Postal::Config.postal.smtp_hostname)
end

context "when relay credentials are provided" do
let(:username) { "relay-user" }
let(:password) { "relay-pass" }

it "starts the SMTP client with login authentication" do
endpoint.start_smtp_session
expect(endpoint.smtp_client).to have_received(:start).with(Postal::Config.postal.smtp_hostname, "relay-user", "relay-pass", :login)
end
end

context "when the SSL mode is Auto" do
it "enables STARTTLS auto " do
client = endpoint.start_smtp_session
Expand Down Expand Up @@ -155,6 +167,16 @@ module SMTPClient
endpoint.start_smtp_session(source_ip_address: ip_address)
expect(endpoint.smtp_client).to have_received(:start).with(ip_address.hostname)
end

context "when relay credentials are provided" do
let(:username) { "relay-user" }
let(:password) { "relay-pass" }

it "starts the SMTP client with the source hostname and login authentication" do
endpoint.start_smtp_session(source_ip_address: ip_address)
expect(endpoint.smtp_client).to have_received(:start).with(ip_address.hostname, "relay-user", "relay-pass", :login)
end
end
end
end

Expand Down
15 changes: 14 additions & 1 deletion spec/lib/smtp_client/server_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,21 @@ module SMTPClient
let(:hostname) { "example.com" }
let(:port) { 25 }
let(:ssl_mode) { SSLModes::AUTO }
let(:username) { nil }
let(:password) { nil }

subject(:server) { described_class.new(hostname, port: port, ssl_mode: ssl_mode) }
subject(:server) { described_class.new(hostname, port: port, ssl_mode: ssl_mode, username: username, password: password) }

describe "attributes" do
context "when credentials are provided" do
let(:username) { "relay-user" }
let(:password) { "relay-pass" }

it "stores the relay credentials" do
expect(server).to have_attributes(username: "relay-user", password: "relay-pass")
end
end
end

describe "#endpoints" do
context "when there are A and AAAA records" do
Expand Down
9 changes: 9 additions & 0 deletions spec/senders/smtp_sender_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -560,5 +560,14 @@
have_attributes(hostname: "test2.example.com", port: 2525, ssl_mode: "TLS"),
]
end

it "returns relays with credentials when configured" do
allow(Postal::Config.postal).to receive(:smtp_relays).and_return([
Hashie::Mash.new(host: "relay.example.com", port: 587, ssl_mode: "TLS", username: "relay-user", password: "relay-pass"),
])
expect(described_class.smtp_relays).to match [
have_attributes(hostname: "relay.example.com", port: 587, ssl_mode: "TLS", username: "relay-user", password: "relay-pass"),
]
end
end
end