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: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

This file contains all the latest changes and updates to Postal.

## [Unreleased]

### Features

* **oidc:** add optional auto-creation of users during OIDC login when enabled via `OIDC_AUTO_CREATE_USERS`
* **oidc:** optionally create a default organization for auto-provisioned users via `OIDC_AUTO_CREATE_ORGANIZATION`

## [3.3.4](https://github.com/postalserver/postal/compare/3.3.3...3.3.4) (2024-06-20)


Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM ruby:3.4.6-slim-bookworm AS base
FROM docker.io/ruby:3.4.6-slim-bookworm AS base

SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN apt-get update \
Expand Down
154 changes: 62 additions & 92 deletions app/lib/smtp_server/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,24 +69,52 @@ def ssl_context
end
end

# --- MODIFIED SECTION ---
def listen
bind_address = ENV.fetch("BIND_ADDRESS", Postal::Config.smtp_server.default_bind_address)
port = ENV.fetch("PORT", Postal::Config.smtp_server.default_port)

@server = TCPServer.open(bind_address, port)
@server.autoclose = false
@server.close_on_exec = false
if defined?(Socket::SOL_SOCKET) && defined?(Socket::SO_KEEPALIVE)
@server.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
end
if defined?(Socket::SOL_TCP) && defined?(Socket::TCP_KEEPIDLE) && defined?(Socket::TCP_KEEPINTVL) && defined?(Socket::TCP_KEEPCNT)
@server.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE, 50)
@server.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL, 10)
@server.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, 5)
@servers = []

# Si "::" -> IPv6 (et peut-être IPv4 selon bindv6only)
if bind_address == "::" || bind_address == "0.0.0.0"
["::", "0.0.0.0"].uniq.each do |addr|
begin
s = TCPServer.new(addr, port)
s.autoclose = false
s.close_on_exec = false
if defined?(Socket::SOL_SOCKET) && defined?(Socket::SO_KEEPALIVE)
s.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
end
if defined?(Socket::SOL_TCP) && defined?(Socket::TCP_KEEPIDLE) && defined?(Socket::TCP_KEEPINTVL) && defined?(Socket::TCP_KEEPCNT)
s.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE, 50)
s.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL, 10)
s.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, 5)
end
@servers << s
logger.info "Listening on #{addr}:#{port}"
rescue => e
logger.warn "Cannot bind to #{addr}:#{port} - #{e.message}"
end
end
else
# Sinon, comportement original
@server = TCPServer.open(bind_address, port)
@server.autoclose = false
@server.close_on_exec = false
if defined?(Socket::SOL_SOCKET) && defined?(Socket::SO_KEEPALIVE)
@server.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
end
if defined?(Socket::SOL_TCP) && defined?(Socket::TCP_KEEPIDLE) && defined?(Socket::TCP_KEEPINTVL) && defined?(Socket::TCP_KEEPCNT)
@server.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE, 50)
@server.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL, 10)
@server.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, 5)
end
@servers = [@server]
logger.info "Listening on #{bind_address}:#{port}"
end

logger.info "Listening on #{bind_address}:#{port}"
end
# --- END MODIFIED SECTION ---

def unlisten
# Instruct the nio loop to unlisten and wake it
Expand All @@ -97,45 +125,37 @@ def unlisten
def run_event_loop
# Set up an instance of nio4r to monitor for connections and data
@io_selector = NIO::Selector.new
# Register the SMTP listener
@io_selector.register(@server, :r)

# Register all listening sockets
@servers.each { |srv| @io_selector.register(srv, :r) }

# Create a hash to contain a buffer for each client.
buffers = Hash.new { |h, k| h[k] = String.new.force_encoding("BINARY") }

loop do
# Wait for an event to occur
@io_selector.select do |monitor|
# Get the IO from the nio monitor
io = monitor.io
# Is this event an incoming connection?
if io.is_a?(TCPServer)

# Accept new client connections from any listener
if @servers.include?(io)
begin
# Accept the connection
new_io = io.accept
increment_prometheus_counter :postal_smtp_server_connections_total
# Get the client's IP address and strip `::ffff:` for consistency.
client_ip_address = new_io.remote_address.ip_address.sub(/\A::ffff:/, "")
if Postal::Config.smtp_server.proxy_protocol?
# If we are using the haproxy proxy protocol, we will be sent the
# client's IP later. Delay the welcome process.
client = Client.new(nil)
if Postal::Config.smtp_server.log_connections?
client.logger&.debug "Connection opened from #{client_ip_address}"
end
client.logger&.debug "Connection opened from #{client_ip_address}" if Postal::Config.smtp_server.log_connections?
else
# We're not using the proxy protocol so we already know the client's IP
client = Client.new(client_ip_address)
if Postal::Config.smtp_server.log_connections?
client.logger&.debug "Connection opened from #{client_ip_address}"
end
# We know who the client is, welcome them.
client.logger&.debug "Client identified as #{client_ip_address}"
new_io.print("220 #{Postal::Config.postal.smtp_hostname} ESMTP Postal/#{client.trace_id}")
end
# Register the client and its socket with nio4r
monitor = @io_selector.register(new_io, :r)
monitor.value = client
rescue StandardError => e
# If something goes wrong, log as appropriate and disconnect the client
if defined?(Sentry)
Sentry.capture_exception(e, extra: { trace_id: begin
client.trace_id
Expand All @@ -145,9 +165,7 @@ def run_event_loop
end
logger.error "An error occurred while accepting a new client."
logger.error "#{e.class}: #{e.message}"
e.backtrace.each do |line|
logger.error line
end
e.backtrace.each { |line| logger.error line }
increment_prometheus_counter :postal_smtp_server_exceptions_total,
labels: { error: e.class.to_s, type: "client-accept" }
begin
Expand All @@ -157,113 +175,75 @@ def run_event_loop
end
end
else
# This event is not an incoming connection so it must be data from a client
# (reste du code original pour gérer les clients)
begin
# Get the client from the nio monitor
client = monitor.value
# For now we assume the connection isn't closed
eof = false
# Is the client negotiating a TLS handshake?

if client.start_tls?
begin
# Can we accept the TLS connection at this time?
io.accept_nonblock
# Increment prometheus
increment_prometheus_counter :postal_smtp_server_tls_connections_total
# We were able to accept the connection, the client is no longer handshaking
client.start_tls = false
rescue IO::WaitReadable, IO::WaitWritable => e
# Could not accept without blocking
# We will try again later
rescue IO::WaitReadable, IO::WaitWritable
next
rescue OpenSSL::SSL::SSLError => e
client.logger&.debug "SSL Negotiation Failed: #{e.message}"
eof = true
end
else
# The client is not negotiating a TLS handshake at this time
begin
# Read 10kiB of data at a time from the socket.
buffers[io] << io.readpartial(10_240)

# There is an extra step for SSL sockets
if io.is_a?(OpenSSL::SSL::SSLSocket)
buffers[io] << io.readpartial(10_240) while io.pending.positive?
end
rescue EOFError, Errno::ECONNRESET, Errno::ETIMEDOUT
# Client went away
eof = true
end

# We line buffer, so look to see if we have received a newline
# and keep doing so until all buffered lines have been processed.
while buffers[io].index("\n")
# Extract the line
line, buffers[io] = buffers[io].split("\n", 2)
# Send the received line to the client object for processing
result = client.handle(line)
# If the client object returned some data, write it back to the client
next if result.nil?

result = [result] unless result.is_a?(Array)
result.compact.each do |iline|
client.logger&.debug "\e[34m=> #{iline.strip}\e[0m"
begin
io.write(iline.to_s + "\r\n")
io.flush
rescue Errno::ECONNRESET
# Client disconnected before we could write response
eof = true
end
end
end

# Did the client request STARTTLS?
if !eof && client.start_tls?
# Deregister the unencrypted IO
@io_selector.deregister(io)
buffers.delete(io)
io = OpenSSL::SSL::SSLSocket.new(io, ssl_context)
# Close the underlying IO when the TLS socket is closed
io.sync_close = true
# Register the new TLS socket with nio
monitor = @io_selector.register(io, :r)
monitor.value = client
end
end

# Has the client requested we close the connection?
if client.finished? || eof
client.logger&.debug "Connection closed"
# Deregister the socket and close it
@io_selector.deregister(io)
buffers.delete(io)
io.close
# If we have no more clients or listeners left, exit the process
if @io_selector.empty?
Process.exit(0)
end
Process.exit(0) if @io_selector.empty?
end
rescue StandardError => e
# Something went wrong, log as appropriate
client_id = client ? client.trace_id : "------"
if defined?(Sentry)
Sentry.capture_exception(e, extra: { trace_id: begin
client&.trace_id
rescue StandardError
nil
end })
Sentry.capture_exception(e, extra: { trace_id: client&.trace_id rescue nil })
end
logger.error "An error occurred while processing data from a client.", trace_id: client_id
logger.error "#{e.class}: #{e.message}", trace_id: client_id
e.backtrace.each do |iline|
logger.error iline, trace_id: client_id
end

e.backtrace.each { |iline| logger.error iline, trace_id: client_id }
increment_prometheus_counter :postal_smtp_server_exceptions_total,
labels: { error: e.class.to_s, type: "data" }

# Close all IO and forget this client
begin
@io_selector.deregister(io)
rescue StandardError
Expand All @@ -275,22 +255,16 @@ def run_event_loop
rescue StandardError
nil
end
if @io_selector.empty?
Process.exit(0)
end
Process.exit(0) if @io_selector.empty?
end
end
end
# If unlisten has been called, stop listening

next unless @unlisten

@io_selector.deregister(@server)
@server.close
# If there's nothing left to do, shut down the process
if @io_selector.empty?
Process.exit(0)
end
# Clear the request
@servers.each { |srv| @io_selector.deregister(srv) rescue nil }
@servers.each { |srv| srv.close rescue nil }
Process.exit(0) if @io_selector.empty?
@unlisten = false
end
end
Expand All @@ -302,16 +276,12 @@ def logger
def register_prometheus_metrics
register_prometheus_counter :postal_smtp_server_connections_total,
docstring: "The number of connections made to the Postal SMTP server."

register_prometheus_counter :postal_smtp_server_exceptions_total,
docstring: "The number of server exceptions encountered by the SMTP server",
labels: [:type, :error]

register_prometheus_counter :postal_smtp_server_tls_connections_total,
docstring: "The number of successfuly TLS connections established"

Client.register_prometheus_metrics
end

end
end
end
Loading