From 0a2205ad316ce9e9161e09a7cd37475c6e1a85c3 Mon Sep 17 00:00:00 2001 From: tda Date: Fri, 7 Nov 2025 13:04:03 +0100 Subject: [PATCH 1/6] Stop tracking docker-compose.yml (local only) --- docker-compose.yml | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 64ec0acfb..000000000 --- a/docker-compose.yml +++ /dev/null @@ -1,28 +0,0 @@ -services: - postal: - image: ${POSTAL_IMAGE} - depends_on: - - mariadb - entrypoint: ["/docker-entrypoint.sh"] - volumes: - - "./docker/ci-config:/config" - environment: - POSTAL_SIGNING_KEY_PATH: /config/signing.key - MAIN_DB_HOST: mariadb - MAIN_DB_USERNAME: root - MESSAGE_DB_HOST: mariadb - MESSAGE_DB_USERNAME: root - LOGGING_ENABLED: "false" - RAILS_ENVIRONMENT: test - RAILS_LOG_ENABLED: "false" - WAIT_FOR_TIMEOUT: 90 - WAIT_FOR_TARGETS: |- - mariadb:3306 - - mariadb: - image: mariadb - restart: always - environment: - MARIADB_DATABASE: postal - MARIADB_ALLOW_EMPTY_PASSWORD: 'yes' - MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: 'yes' From 4626dc6a91d2655340b8e320d210eaa56d290610 Mon Sep 17 00:00:00 2001 From: tda Date: Fri, 7 Nov 2025 15:41:51 +0100 Subject: [PATCH 2/6] Auto add user with OIDC when It doesn't exist --- CHANGELOG.md | 6 +++ app/models/user.rb | 56 ++++++++++++++++++++++-- doc/config/environment-variables.md | 1 + doc/config/yaml.yml | 2 + lib/postal/config_schema.rb | 5 +++ spec/models/user/oidc_spec.rb | 66 ++++++++++++++++++++++++++--- 6 files changed, 126 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b78a9257..38fa6179a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ 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` + ## [3.3.4](https://github.com/postalserver/postal/compare/3.3.3...3.3.4) (2024-06-20) diff --git a/app/models/user.rb b/app/models/user.rb index 8bf48aac1..6740014d3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -116,15 +116,23 @@ def find_from_oidc(auth, logger: nil) end end - # now, if we still don't have a user, we're not going to create one so we'll just - # return nil (we might auto create users in the future but not right now) - return if user.nil? + # If we still don't have a user and auto creation is enabled, create one from the OIDC details. + if user.nil? + if config.auto_create_users? + user = auto_create_user_from_oidc(uid, config, oidc_name, oidc_email_address, logger) + else + logger&.debug "OIDC auto user creation disabled; not creating user for #{oidc_email_address || uid}" + end + return if user.nil? + end # otherwise, let's update our user as appropriate user.oidc_uid = uid user.oidc_issuer = config.issuer user.email_address = oidc_email_address if oidc_email_address.present? - user.first_name, user.last_name = oidc_name.split(/\s+/, 2) if oidc_name.present? + if oidc_name.present? + user.first_name, user.last_name = derive_user_names_from_oidc(oidc_name, user.email_address) + end user.password = nil user.save! @@ -132,6 +140,46 @@ def find_from_oidc(auth, logger: nil) user end + private + + def auto_create_user_from_oidc(uid, config, oidc_name, oidc_email_address, logger) + unless oidc_email_address.present? + logger&.warn "OIDC auto user creation failed for UID #{uid}: no e-mail address provided" + return nil + end + + first_name, last_name = derive_user_names_from_oidc(oidc_name, oidc_email_address) + user = new( + email_address: oidc_email_address, + first_name: first_name, + last_name: last_name + ) + user.oidc_uid = uid + user.oidc_issuer = config.issuer + user.password = nil + user.save! + logger&.info "OIDC auto user creation succeeded for #{oidc_email_address} (user ID: #{user.id}) with Firstname: #{first_name}, Lastname: #{last_name}" + user + rescue ActiveRecord::RecordInvalid => e + logger&.error "OIDC auto user creation failed for #{oidc_email_address}: #{e.message}" + nil + end + + def derive_user_names_from_oidc(oidc_name, oidc_email_address) + raw_name = oidc_name.to_s.strip + if raw_name.present? + first_name, last_name = raw_name.split(/\s+/, 2) + else + local_part = oidc_email_address.to_s.split("@", 2).first.to_s + fallback_name = local_part.tr("._-", " ").strip + first_name, last_name = fallback_name.split(/\s+/, 2) + end + + first_name = first_name.presence || "OIDC" + last_name = last_name.presence || first_name + [first_name, last_name] + end + end end diff --git a/doc/config/environment-variables.md b/doc/config/environment-variables.md index 940424e04..4cf7b7754 100644 --- a/doc/config/environment-variables.md +++ b/doc/config/environment-variables.md @@ -101,6 +101,7 @@ This document contains all the environment variables which are available for thi | `MIGRATION_WAITER_SLEEP_TIME` | Integer | The number of seconds to wait between each migration check | 2 | | `OIDC_ENABLED` | Boolean | Enable OIDC authentication | false | | `OIDC_LOCAL_AUTHENTICATION_ENABLED` | Boolean | When enabled, users with passwords will still be able to login locally. If disable, only OpenID Connect will be available. | true | +| `OIDC_AUTO_CREATE_USERS` | Boolean | Automatically create a user record when a valid OIDC login is received but no matching user exists | false | | `OIDC_NAME` | String | The name of the OIDC provider as shown in the UI | OIDC Provider | | `OIDC_ISSUER` | String | The OIDC issuer URL | | | `OIDC_IDENTIFIER` | String | The client ID for OIDC | | diff --git a/doc/config/yaml.yml b/doc/config/yaml.yml index f3a735a9f..763a14727 100644 --- a/doc/config/yaml.yml +++ b/doc/config/yaml.yml @@ -229,6 +229,8 @@ oidc: enabled: false # When enabled, users with passwords will still be able to login locally. If disable, only OpenID Connect will be available. local_authentication_enabled: true + # Automatically create a user record when a valid OIDC login is received but no user exists + auto_create_users: false # The name of the OIDC provider as shown in the UI name: OIDC Provider # The OIDC issuer URL diff --git a/lib/postal/config_schema.rb b/lib/postal/config_schema.rb index e3c6415d5..af75f55d8 100644 --- a/lib/postal/config_schema.rb +++ b/lib/postal/config_schema.rb @@ -530,6 +530,11 @@ module Postal default true end + boolean :auto_create_users do + description "Automatically create a user record when a valid OIDC login is received but no matching user exists" + default false + end + string :name do description "The name of the OIDC provider as shown in the UI" default "OIDC Provider" diff --git a/spec/models/user/oidc_spec.rb b/spec/models/user/oidc_spec.rb index a03668cbb..3660f6e31 100644 --- a/spec/models/user/oidc_spec.rb +++ b/spec/models/user/oidc_spec.rb @@ -24,6 +24,9 @@ allow(Postal::Config.oidc).to receive(:enabled?).and_return(true) allow(Postal::Config.oidc).to receive(:issuer).and_return(issuer) allow(Postal::Config.oidc).to receive(:email_address_field).and_return("email") + allow(Postal::Config.oidc).to receive(:uid_field).and_return("sub") + allow(Postal::Config.oidc).to receive(:name_field).and_return("name") + allow(Postal::Config.oidc).to receive(:auto_create_users?).and_return(false) end let(:uid) { "abcdef" } @@ -53,6 +56,17 @@ expect(@existing_user.email_address).to eq "test@example.com" end + context "when the OIDC name contains a single word" do + let(:oidc_name) { "johnny" } + + it "duplicates the value for the last name" do + result + @existing_user.reload + expect(@existing_user.first_name).to eq "johnny" + expect(@existing_user.last_name).to eq "johnny" + end + end + it "logs" do result expect(logger).to have_logged(/found user with UID abcdef/i) @@ -100,14 +114,54 @@ end context "when there is no user which matches the email address" do - it "returns nil" do - expect(result).to be_nil + context "and auto creation is disabled" do + it "returns nil" do + expect(result).to be_nil + end + + it "logs" do + result + expect(logger).to have_logged(/no user with UID abcdef/) + expect(logger).to have_logged(/no user with e-mail address/) + end end - it "logs" do - result - expect(logger).to have_logged(/no user with UID abcdef/) - expect(logger).to have_logged(/no user with e-mail address/) + context "and auto creation is enabled" do + before do + allow(Postal::Config.oidc).to receive(:auto_create_users?).and_return(true) + end + + it "creates a new user" do + expect { result }.to change(User, :count).by(1) + expect(result.email_address).to eq oidc_email + expect(result.oidc_uid).to eq uid + expect(result.oidc_issuer).to eq issuer + expect(result.first_name).to eq "John" + expect(result.last_name).to eq "Smith" + end + + it "logs the creation" do + result + expect(logger).to have_logged(/OIDC auto user creation succeeded/i) + end + + context "when no name is provided" do + let(:oidc_name) { nil } + + it "derives a name from the e-mail address" do + expect(result.first_name).to eq "test" + expect(result.last_name).to eq "test" + end + end + + context "when no e-mail is provided" do + let(:oidc_email) { nil } + + it "cannot create a user" do + expect(result).to be_nil + expect(logger).to have_logged(/no e-mail address provided/) + end + end end end end From 4231ad2968ece37868e93dd3b8b781cc9e768f92 Mon Sep 17 00:00:00 2001 From: tda Date: Fri, 7 Nov 2025 16:08:12 +0100 Subject: [PATCH 3/6] feat: add automatic default organization creation for OIDC users --- CHANGELOG.md | 1 + app/models/user.rb | 13 +++++++++++++ doc/config/environment-variables.md | 2 ++ doc/config/yaml.yml | 4 ++++ lib/postal/config_schema.rb | 10 ++++++++++ spec/models/user/oidc_spec.rb | 21 +++++++++++++++++++++ 6 files changed, 51 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38fa6179a..3381914d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ This file contains all the latest changes and updates to Postal. ### 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) diff --git a/app/models/user.rb b/app/models/user.rb index 6740014d3..b99fbd2d6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -159,6 +159,7 @@ def auto_create_user_from_oidc(uid, config, oidc_name, oidc_email_address, logge user.password = nil user.save! logger&.info "OIDC auto user creation succeeded for #{oidc_email_address} (user ID: #{user.id}) with Firstname: #{first_name}, Lastname: #{last_name}" + auto_create_organization_for(user, config, logger) if config.auto_create_organization? user rescue ActiveRecord::RecordInvalid => e logger&.error "OIDC auto user creation failed for #{oidc_email_address}: #{e.message}" @@ -180,6 +181,18 @@ def derive_user_names_from_oidc(oidc_name, oidc_email_address) [first_name, last_name] end + def auto_create_organization_for(user, config, logger) + organization_name = config.auto_created_organization_name.presence || "My organization" + organization = Organization.new(name: organization_name, owner: user) + organization.save! + organization.organization_users.create!(user: user, admin: true, all_servers: true) + logger&.info "OIDC auto organization creation succeeded for user #{user.id} (organization ID: #{organization.id})" + organization + rescue ActiveRecord::RecordInvalid => e + logger&.error "OIDC auto organization creation failed for user #{user.id}: #{e.message}" + nil + end + end end diff --git a/doc/config/environment-variables.md b/doc/config/environment-variables.md index 4cf7b7754..2abc790bd 100644 --- a/doc/config/environment-variables.md +++ b/doc/config/environment-variables.md @@ -102,6 +102,8 @@ This document contains all the environment variables which are available for thi | `OIDC_ENABLED` | Boolean | Enable OIDC authentication | false | | `OIDC_LOCAL_AUTHENTICATION_ENABLED` | Boolean | When enabled, users with passwords will still be able to login locally. If disable, only OpenID Connect will be available. | true | | `OIDC_AUTO_CREATE_USERS` | Boolean | Automatically create a user record when a valid OIDC login is received but no matching user exists | false | +| `OIDC_AUTO_CREATE_ORGANIZATION` | Boolean | Automatically create a default organization for auto-created OIDC users | false | +| `OIDC_AUTO_CREATED_ORGANIZATION_NAME` | String | Name to assign to automatically created organizations | My organization | | `OIDC_NAME` | String | The name of the OIDC provider as shown in the UI | OIDC Provider | | `OIDC_ISSUER` | String | The OIDC issuer URL | | | `OIDC_IDENTIFIER` | String | The client ID for OIDC | | diff --git a/doc/config/yaml.yml b/doc/config/yaml.yml index 763a14727..c24bf244d 100644 --- a/doc/config/yaml.yml +++ b/doc/config/yaml.yml @@ -231,6 +231,10 @@ oidc: local_authentication_enabled: true # Automatically create a user record when a valid OIDC login is received but no user exists auto_create_users: false + # Automatically create a default organization for auto-created users + auto_create_organization: false + # The name to use for automatically created organizations + auto_created_organization_name: My organization # The name of the OIDC provider as shown in the UI name: OIDC Provider # The OIDC issuer URL diff --git a/lib/postal/config_schema.rb b/lib/postal/config_schema.rb index af75f55d8..508e276bc 100644 --- a/lib/postal/config_schema.rb +++ b/lib/postal/config_schema.rb @@ -535,6 +535,16 @@ module Postal default false end + boolean :auto_create_organization do + description "Automatically create a default organization for auto-created OIDC users" + default false + end + + string :auto_created_organization_name do + description "The name to use for automatically created organizations" + default "My organization" + end + string :name do description "The name of the OIDC provider as shown in the UI" default "OIDC Provider" diff --git a/spec/models/user/oidc_spec.rb b/spec/models/user/oidc_spec.rb index 3660f6e31..bfc55bca8 100644 --- a/spec/models/user/oidc_spec.rb +++ b/spec/models/user/oidc_spec.rb @@ -27,6 +27,8 @@ allow(Postal::Config.oidc).to receive(:uid_field).and_return("sub") allow(Postal::Config.oidc).to receive(:name_field).and_return("name") allow(Postal::Config.oidc).to receive(:auto_create_users?).and_return(false) + allow(Postal::Config.oidc).to receive(:auto_create_organization?).and_return(false) + allow(Postal::Config.oidc).to receive(:auto_created_organization_name).and_return("My organization") end let(:uid) { "abcdef" } @@ -162,6 +164,25 @@ expect(logger).to have_logged(/no e-mail address provided/) end end + + context "when organization auto creation is enabled" do + let(:organization_name) { "My organization" } + + before do + allow(Postal::Config.oidc).to receive(:auto_create_organization?).and_return(true) + allow(Postal::Config.oidc).to receive(:auto_created_organization_name).and_return(organization_name) + end + + it "creates an organization owned by the new user" do + expect { result }.to change(Organization, :count).by(1) + organization = Organization.last + expect(organization.name).to eq organization_name + expect(organization.owner).to eq result + expect(result.organizations).to include(organization) + expect(organization.organization_users.first.user).to eq result + expect(organization.organization_users.first.admin).to be true + end + end end end end From 3e09c6e007e0d34d51351cebd08f113ee64cbefa Mon Sep 17 00:00:00 2001 From: tda Date: Mon, 10 Nov 2025 00:22:28 +0100 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20corriger=20la=20d=C3=A9claration=20d?= =?UTF-8?q?e=20l'image=20de=20base=20dans=20le=20Dockerfile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5adcb179c..001867408 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 \ From ef238ff234ea6969f941b85b3dabf046bc9e9544 Mon Sep 17 00:00:00 2001 From: tda Date: Tue, 11 Nov 2025 00:24:09 +0100 Subject: [PATCH 5/6] Add ipv6 support (For server binding) --- app/lib/smtp_server/server.rb | 154 ++++++++++++++-------------------- config/puma.rb | 3 + 2 files changed, 65 insertions(+), 92 deletions(-) diff --git a/app/lib/smtp_server/server.rb b/app/lib/smtp_server/server.rb index 90902b5a8..8508715b2 100644 --- a/app/lib/smtp_server/server.rb +++ b/app/lib/smtp_server/server.rb @@ -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 @@ -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 @@ -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 @@ -157,54 +175,36 @@ 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" @@ -212,58 +212,38 @@ def run_event_loop 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 @@ -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 @@ -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 \ No newline at end of file diff --git a/config/puma.rb b/config/puma.rb index e6bba3007..56a48e9f3 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -6,6 +6,9 @@ threads threads_count, threads_count bind_address = ENV.fetch("BIND_ADDRESS", Postal::Config.web_server.default_bind_address) bind_port = ENV.fetch("PORT", Postal::Config.web_server.default_port) +if bind_address.include?(":") && !bind_address.start_with?("[") + bind_address = "[#{bind_address}]" +end bind "tcp://#{bind_address}:#{bind_port}" environment Postal::Config.rails.environment || "development" prune_bundler From 96faeb687fbd6412fc3d60a09c564ead3374d5f7 Mon Sep 17 00:00:00 2001 From: tda Date: Tue, 11 Nov 2025 00:25:29 +0100 Subject: [PATCH 6/6] Revert "Stop tracking docker-compose.yml (local only)" This reverts commit 0a2205ad316ce9e9161e09a7cd37475c6e1a85c3. Mistake in deletion of docker compose# --- docker-compose.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..64ec0acfb --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,28 @@ +services: + postal: + image: ${POSTAL_IMAGE} + depends_on: + - mariadb + entrypoint: ["/docker-entrypoint.sh"] + volumes: + - "./docker/ci-config:/config" + environment: + POSTAL_SIGNING_KEY_PATH: /config/signing.key + MAIN_DB_HOST: mariadb + MAIN_DB_USERNAME: root + MESSAGE_DB_HOST: mariadb + MESSAGE_DB_USERNAME: root + LOGGING_ENABLED: "false" + RAILS_ENVIRONMENT: test + RAILS_LOG_ENABLED: "false" + WAIT_FOR_TIMEOUT: 90 + WAIT_FOR_TARGETS: |- + mariadb:3306 + + mariadb: + image: mariadb + restart: always + environment: + MARIADB_DATABASE: postal + MARIADB_ALLOW_EMPTY_PASSWORD: 'yes' + MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: 'yes'