Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package postsrsd for openSUSE:Factory 
checked in at 2026-08-14 22:06:58
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/postsrsd (Old)
 and      /work/SRC/openSUSE:Factory/.postsrsd.new.1258 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "postsrsd"

Fri Aug 14 22:06:58 2026 rev:23 rq:1371023 version:2.4.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/postsrsd/postsrsd.changes        2026-07-17 
20:38:24.962354071 +0200
+++ /work/SRC/openSUSE:Factory/.postsrsd.new.1258/postsrsd.changes      
2026-08-14 22:07:22.707873101 +0200
@@ -1,0 +2,9 @@
+Thu Aug 13 10:29:34 UTC 2026 - Jan Engelhardt <[email protected]>
+
+- Update to release 2.4.0
+  * New configuration option ``connection-limit`` to prevent DoS
+    attacks by malicious clients repeatedly connecting to PostSRSd.
+  * New configuration option ``milter-recipient-limit`` to keep the
+    number of recipients per SMTP session sane.
+
+-------------------------------------------------------------------

Old:
----
  2.3.0.tar.gz

New:
----
  2.4.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ postsrsd.spec ++++++
--- /var/tmp/diff_new_pack.81HPbR/_old  2026-08-14 22:07:23.836914208 +0200
+++ /var/tmp/diff_new_pack.81HPbR/_new  2026-08-14 22:07:23.838914281 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           postsrsd
-Version:        2.3.0
+Version:        2.4.0
 Release:        0
 Summary:        Sender Rewriting Support for postfix
 License:        GPL-2.0-only

++++++ 2.3.0.tar.gz -> 2.4.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/.pre-commit-config.yaml 
new/postsrsd-2.4.0/.pre-commit-config.yaml
--- old/postsrsd-2.3.0/.pre-commit-config.yaml  2026-07-17 19:04:57.000000000 
+0200
+++ new/postsrsd-2.4.0/.pre-commit-config.yaml  2026-08-12 10:28:49.000000000 
+0200
@@ -5,8 +5,11 @@
     rev: v6.0.0
     hooks:
     -   id: trailing-whitespace
+        exclude: ^fuzz/(?:seeds|dict)
     -   id: end-of-file-fixer
+        exclude: ^fuzz/(?:seeds|dict)
     -   id: check-yaml
+        exclude: ^fuzz/(?:seeds|dict)
     -   id: check-added-large-files
 -   repo: https://github.com/pre-commit/mirrors-clang-format
     rev: v22.1.8
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/CHANGELOG.rst 
new/postsrsd-2.4.0/CHANGELOG.rst
--- old/postsrsd-2.3.0/CHANGELOG.rst    2026-07-17 19:04:57.000000000 +0200
+++ new/postsrsd-2.4.0/CHANGELOG.rst    2026-08-12 10:28:49.000000000 +0200
@@ -7,6 +7,27 @@
 Changelog
 #########
 
+2.4.0
+=====
+
+Fixed
+-----
+
+* A few minor code defects have been fixed to contribute to defense in depth.
+
+Added
+-----
+
+* New configuration option ``connection-limit`` to prevent DoS attacks by
+  malicious clients repeatedly connecting to PostSRSd.
+* New configuration option ``milter-recipient-limit`` to keep the number
+  of recipients per SMTP session sane.
+
+Changed
+-------
+
+* The milter protocol implementation is no longer considered experimental.
+
 2.3.0
 =====
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/CMakeLists.txt 
new/postsrsd-2.4.0/CMakeLists.txt
--- old/postsrsd-2.3.0/CMakeLists.txt   2026-07-17 19:04:57.000000000 +0200
+++ new/postsrsd-2.4.0/CMakeLists.txt   2026-08-12 10:28:49.000000000 +0200
@@ -17,7 +17,7 @@
 cmake_minimum_required(VERSION 3.24...4.4)
 project(
     postsrsd
-    VERSION 2.3.0
+    VERSION 2.4.0
     LANGUAGES C
     DESCRIPTION "Sender Rewriting Scheme daemon for Postfix"
     HOMEPAGE_URL "https://github.com/roehling/postsrsd";
@@ -54,6 +54,8 @@
     CACHE STRING "The default unprivileged user as which PostSRSd will run"
 )
 
+option(BUILD_FUZZING "Build fuzzing harnesses" OFF)
+
 option(WITH_SQLITE
        "Enable sqlite-based storage for opaque SRS tokens (requires sqlite3)"
        OFF
@@ -354,12 +356,6 @@
 
 if(DEVELOPER_BUILD AND CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
     add_compile_options(-Wall -Wextra -Werror)
-    if(CMAKE_C_COMPILER_ID STREQUAL "GNU" AND CMAKE_C_COMPILER_VERSION
-                                              VERSION_LESS 11.0
-    )
-        # isspace() / isalnum() / tolower() / toupper() trigger this warning
-        add_compile_options(-Wno-char-subscripts)
-    endif()
 endif()
 
 add_executable(
@@ -507,6 +503,10 @@
     add_subdirectory(tests)
 endif()
 
+if(BUILD_FUZZING)
+    add_subdirectory(fuzz)
+endif()
+
 feature_summary(WHAT ENABLED_FEATURES DISABLED_FEATURES)
 
 if(DEFINED INIT_FLAVOR)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/README.rst 
new/postsrsd-2.4.0/README.rst
--- old/postsrsd-2.3.0/README.rst       2026-07-17 19:04:57.000000000 +0200
+++ new/postsrsd-2.4.0/README.rst       2026-08-12 10:28:49.000000000 +0200
@@ -145,20 +145,15 @@
 
 .. _example: doc/postsrsd.conf
 
-Experimental Milter Support
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Milter Support
+~~~~~~~~~~~~~~
 
-PostSRSd 2.x has added optional support for the Milter protocol. You can set
-the ``milter`` option in ``postsrsd.conf`` and add the corresponding line to
-your ``etc/postfix/main.cf``::
+PostSRSd 2.x has added support for the Milter protocol. You can set the
+``milter`` option in ``postsrsd.conf`` and add the corresponding line to your
+``etc/postfix/main.cf``::
 
     smtpd_milters = unix:srs_milter
 
-Note that the Milter code is less tested and should be considered experimental
-for now and not ready for production. Feel free to report bugs or open pull
-requests if you try it out, though. Until PostSRSd 2.1, the milter support was
-implemented with the external libMilter library from Sendmail.
-
 Inotify Support
 ~~~~~~~~~~~~~~~
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/data/postsrsd.conf.in 
new/postsrsd-2.4.0/data/postsrsd.conf.in
--- old/postsrsd-2.3.0/data/postsrsd.conf.in    2026-07-17 19:04:57.000000000 
+0200
+++ new/postsrsd-2.4.0/data/postsrsd.conf.in    2026-08-12 10:28:49.000000000 
+0200
@@ -83,6 +83,17 @@
 #
 #keep-alive = 30
 
+# Connection limit.
+# This is a hard limit for the number of concurrent connections. It is
+# a fail-safe to prevent PostSRSd from forking an unbounded number of
+# child processes. Once this limit is reached, PostSRSd will accept but
+# immediately close incoming connections without serving requests.
+#
+# Default:
+#     connection-limit = 200
+#
+#connection-limit = 200
+
 # Secret keys for signing and verifying SRS addresses.
 # Rewritten addresses are tagged with a truncated HMAC-SHA1 signature, to
 # prevent tampering and forged envelope addresses. You can have more than
@@ -165,6 +176,16 @@
 #
 #milter-rewrite-local = off
 
+# Recipient limit per milter transaction
+# PostSRSd rejects emails which exceed this recipient limit, to prevent
+# broken or malicious MTAs from consuming arbitrary amounts of memory. The
+# default limit matches the Postfix default limit for SMTP transactions.
+#
+# Default:
+#     milter-recipient-limit = 1000
+#
+#milter-recipient-limit = 1000
+
 # SRS tag separator
 # This is the character following the initial SRS0 or SRS1 tag of a generated
 # sender address. Valid separators are "=", "+", and "-". Unless you have a
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/doc/postsrsd.conf 
new/postsrsd-2.4.0/doc/postsrsd.conf
--- old/postsrsd-2.3.0/doc/postsrsd.conf        2026-07-17 19:04:57.000000000 
+0200
+++ new/postsrsd-2.4.0/doc/postsrsd.conf        2026-08-12 10:28:49.000000000 
+0200
@@ -83,6 +83,17 @@
 #
 #keep-alive = 30
 
+# Connection limit.
+# This is a hard limit for the number of concurrent connections. It is
+# a fail-safe to prevent PostSRSd from forking an unbounded number of
+# child processes. Once this limit is reached, PostSRSd will accept but
+# immediately close incoming connections without serving requests.
+#
+# Default:
+#     connection-limit = 200
+#
+#connection-limit = 200
+
 # Secret keys for signing and verifying SRS addresses.
 # Rewritten addresses are tagged with a truncated HMAC-SHA1 signature, to
 # prevent tampering and forged envelope addresses. You can have more than
@@ -165,6 +176,16 @@
 #
 #milter-rewrite-local = off
 
+# Recipient limit per milter transaction
+# PostSRSd rejects emails which exceed this recipient limit, to prevent
+# broken or malicious MTAs from consuming arbitrary amounts of memory. The
+# default limit matches the Postfix default limit for SMTP transactions.
+#
+# Default:
+#     milter-recipient-limit = 1000
+#
+#milter-recipient-limit = 1000
+
 # SRS tag separator
 # This is the character following the initial SRS0 or SRS1 tag of a generated
 # sender address. Valid separators are "=", "+", and "-". Unless you have a
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/CMakeLists.txt 
new/postsrsd-2.4.0/fuzz/CMakeLists.txt
--- old/postsrsd-2.3.0/fuzz/CMakeLists.txt      1970-01-01 01:00:00.000000000 
+0100
+++ new/postsrsd-2.4.0/fuzz/CMakeLists.txt      2026-08-12 10:28:49.000000000 
+0200
@@ -0,0 +1,93 @@
+# PostSRSd - Sender Rewriting Scheme daemon for Postfix
+# Copyright 2012-2026 Timo Röhling <[email protected]>
+# SPDX-License-Identifier: GPL-3.0-only
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, version 3.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+find_program(AFL_FUZZ afl-fuzz)
+
+add_library(
+    postsrsd_fuzz
+    fuzz.c
+    ${PROJECT_SOURCE_DIR}/src/config.c
+    ${PROJECT_SOURCE_DIR}/src/database.c
+    ${PROJECT_SOURCE_DIR}/src/endpoint.c
+    ${PROJECT_SOURCE_DIR}/src/main.c
+    ${PROJECT_SOURCE_DIR}/src/milter.c
+    ${PROJECT_SOURCE_DIR}/src/netstring.c
+    ${PROJECT_SOURCE_DIR}/src/sha1.c
+    ${PROJECT_SOURCE_DIR}/src/srs.c
+    ${PROJECT_SOURCE_DIR}/src/srs2.c
+    ${PROJECT_SOURCE_DIR}/src/util.c
+)
+target_compile_definitions(
+    postsrsd_fuzz PRIVATE POSTSRSD_FUZZING _GNU_SOURCE _FILE_OFFSET_BITS=64
+)
+target_include_directories(
+    postsrsd_fuzz PUBLIC ${PROJECT_SOURCE_DIR}/src ${PROJECT_BINARY_DIR}
+)
+target_compile_options(
+    postsrsd_fuzz PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer
+)
+target_link_options(postsrsd_fuzz PRIVATE -fsanitize=address,undefined)
+target_compile_features(postsrsd_fuzz PRIVATE c_std_99)
+target_link_libraries(
+    postsrsd_fuzz
+    PRIVATE libconfuse::confuse
+            $<$<BOOL:${WITH_SQLITE}>:sqlite3::sqlite3>
+            $<$<BOOL:${WITH_REDIS}>:${HIREDIS_TARGET}>
+            $<$<BOOL:${WITH_SECCOMP}>:seccomp::seccomp>
+            ${LIBSOCKET}
+            ${LIBNSL}
+            ${LIBNETWORK}
+)
+
+function(add_fuzzer_harness name)
+    cmake_parse_arguments(arg "" "SEED;DICT" "" ${ARGN})
+    add_executable(${name}_harness ${name}.c)
+    target_compile_options(
+        ${name}_harness PRIVATE -fsanitize=fuzzer,address,undefined
+                                -fno-omit-frame-pointer
+    )
+    target_link_options(
+        ${name}_harness PRIVATE -fsanitize=fuzzer,address,undefined
+    )
+    target_link_libraries(${name}_harness PRIVATE postsrsd_fuzz)
+    file(MAKE_DIRECTORY ${PROJECT_BINARY_DIR}/FUZZING/${name})
+    set(fuzzer_call ${AFL_FUZZ} -G 4096 -o
+                    "${PROJECT_BINARY_DIR}/FUZZING/${name}"
+    )
+    if(arg_SEED)
+        list(APPEND fuzzer_call -i
+             "${CMAKE_CURRENT_SOURCE_DIR}/seeds/${arg_SEED}"
+        )
+    endif()
+    if(arg_DICT)
+        list(APPEND fuzzer_call -x
+             "${CMAKE_CURRENT_SOURCE_DIR}/dict/${arg_DICT}"
+        )
+    endif()
+    list(APPEND fuzzer_call "--" "$<TARGET_FILE:${name}_harness>")
+    message(STATUS "${fuzzer_call}")
+    add_custom_target(
+        ${name}
+        "${fuzzer_call}"
+        COMMAND_EXPAND_LISTS
+    )
+endfunction()
+
+add_fuzzer_harness(fuzz_netstring SEED netstring)
+add_fuzzer_harness(fuzz_srs SEED srs DICT srs)
+add_fuzzer_harness(fuzz_socketmap SEED socketmap DICT socketmap)
+add_fuzzer_harness(fuzz_milter SEED milter DICT milter)
+add_fuzzer_harness(fuzz_milter_parsers SEED milter_parsers)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/FUZZ_README.rst 
new/postsrsd-2.4.0/fuzz/FUZZ_README.rst
--- old/postsrsd-2.3.0/fuzz/FUZZ_README.rst     1970-01-01 01:00:00.000000000 
+0100
+++ new/postsrsd-2.4.0/fuzz/FUZZ_README.rst     2026-08-12 10:28:49.000000000 
+0200
@@ -0,0 +1,21 @@
+..
+    PostSRSd - Sender Rewriting Scheme daemon for Postfix
+    Copyright 2012-2026 Timo Röhling <[email protected]>
+    SPDX-License-Identifier: GPL-3.0-only
+
+======================
+PostSRSd Fuzzing Notes
+======================
+
+This is a simple fuzzing harness for AFL_ which I used to shake out some bugs::
+
+    cd path/to/source
+    mkdir _build && cd _build
+    cmake .. -DCMAKE_BUILD_TYPE=RelWithDebInfo 
-DCMAKE_C_COMPILER=/usr/bin/afl-clang-lto -DBUILD_FUZZING=ON
+    cmake --build . --target $FUZZING_TARGET
+    cmake --build . --target
+
+Currently implemented fuzzing targets are:
+``fuzz_netstring``, ``fuzz_srs``, ``fuzz_socketmap``.
+
+.. _AFL: https://aflplus.plus/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/dict/milter 
new/postsrsd-2.4.0/fuzz/dict/milter
--- old/postsrsd-2.3.0/fuzz/dict/milter 1970-01-01 01:00:00.000000000 +0100
+++ new/postsrsd-2.4.0/fuzz/dict/milter 2026-08-12 10:28:49.000000000 +0200
@@ -0,0 +1,13 @@
+"O"
+"C"
+"H"
+"M"
+"R"
+"L"
+"N"
+"B"
+"E"
+"A"
+"Q"
+"D"
+"6"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/dict/socketmap 
new/postsrsd-2.4.0/fuzz/dict/socketmap
--- old/postsrsd-2.3.0/fuzz/dict/socketmap      1970-01-01 01:00:00.000000000 
+0100
+++ new/postsrsd-2.4.0/fuzz/dict/socketmap      2026-08-12 10:28:49.000000000 
+0200
@@ -0,0 +1,4 @@
+"forward "
+"reverse "
+"@"
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/dict/srs 
new/postsrsd-2.4.0/fuzz/dict/srs
--- old/postsrsd-2.3.0/fuzz/dict/srs    1970-01-01 01:00:00.000000000 +0100
+++ new/postsrsd-2.4.0/fuzz/dict/srs    2026-08-12 10:28:49.000000000 +0200
@@ -0,0 +1,7 @@
+"SRS0"
+"SRS1"
+"="
+"+"
+"-"
+"@"
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/fuzz.c 
new/postsrsd-2.4.0/fuzz/fuzz.c
--- old/postsrsd-2.3.0/fuzz/fuzz.c      1970-01-01 01:00:00.000000000 +0100
+++ new/postsrsd-2.4.0/fuzz/fuzz.c      2026-08-12 10:28:49.000000000 +0200
@@ -0,0 +1,104 @@
+/* PostSRSd - Sender Rewriting Scheme daemon for Postfix
+ * Copyright 2012-2026 Timo Röhling <[email protected]>
+ * SPDX-License-Identifier: GPL-3.0-only
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "fuzz.h"
+
+#include <stdlib.h>
+#include <sys/socket.h>
+#include <sys/wait.h>
+
+postsrsd_t* fuzz_state_create()
+{
+    postsrsd_t* state = malloc(sizeof(postsrsd_t));
+    if (state == NULL)
+        return NULL;
+    init_state(state);
+
+    state->cfg = config_defaults();
+    if (state->cfg == NULL)
+        goto fail;
+    cfg_setstr(state->cfg, "chroot-dir", "");
+    cfg_setstr(state->cfg, "unprivileged-user", "");
+    cfg_setbool(state->cfg, "seccomp", cfg_false);
+    cfg_setint(state->cfg, "keep-alive", 1);
+    cfg_setint(state->cfg, "milter-recipient-limit", 1000);
+    cfg_setbool(state->cfg, "always-rewrite", cfg_true);
+
+    state->srs = srs_new();
+    if (state->srs == NULL)
+        goto fail;
+    srs_add_secret(state->srs, "tops3cr3t");
+
+    state->srs_domain = strdup("example.com");
+    if (state->srs_domain == NULL)
+        goto fail;
+
+    state->local_domains = domain_set_create();
+    if (state->local_domains == NULL)
+        goto fail;
+    domain_set_add(state->local_domains, "example.com");
+    return state;
+
+fail:
+    finalize_state(state);
+    free(state);
+    return NULL;
+}
+
+void fuzz_state_destroy(postsrsd_t* state)
+{
+    finalize_state(state);
+    free(state);
+}
+
+void fuzz_client(network_client_t client, const uint8_t* data, size_t size)
+{
+    int fds[2];
+    if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds) < 0)
+        abort();
+    pid_t pid = fork();
+    if (pid < 0)
+        abort();
+    if (pid == 0)
+    {
+        close(fds[0]);
+        postsrsd_t* state = fuzz_state_create();
+        if (state == NULL)
+            _exit(1);
+        handle_socketmap_client(state, fds[1]);
+        fuzz_state_destroy(state);
+        close(fds[1]);
+        _exit(0);
+    }
+    close(fds[1]);
+    if (size > 4096)
+        size = 4096;
+    if (write(fds[0], data, size) != (ssize_t)size)
+        abort();
+    shutdown(fds[0], SHUT_WR);
+    char buffer[4096];
+    while (read(fds[0], buffer, sizeof(buffer)) > 0) /* do nothing */
+        ;
+    close(fds[0]);
+    int status;
+    if (waitpid(pid, &status, 0) < 0)
+        abort();
+    if (WIFSIGNALED(status))
+        abort();
+    if (WIFEXITED(status) && WEXITSTATUS(status) != 0)
+        abort();
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/fuzz.h 
new/postsrsd-2.4.0/fuzz/fuzz.h
--- old/postsrsd-2.3.0/fuzz/fuzz.h      1970-01-01 01:00:00.000000000 +0100
+++ new/postsrsd-2.4.0/fuzz/fuzz.h      2026-08-12 10:28:49.000000000 +0200
@@ -0,0 +1,29 @@
+/* PostSRSd - Sender Rewriting Scheme daemon for Postfix
+ * Copyright 2012-2026 Timo Röhling <[email protected]>
+ * SPDX-License-Identifier: GPL-3.0-only
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+#ifndef FUZZ_H
+#define FUZZ_H
+
+#include <main.h>
+#include <stdint.h>
+
+typedef void (*network_client_t)(postsrsd_t*, int);
+
+postsrsd_t* fuzz_state_create();
+void fuzz_state_destroy(postsrsd_t* state);
+void fuzz_client(network_client_t client, const uint8_t* data, size_t size);
+
+#endif
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/fuzz_milter.c 
new/postsrsd-2.4.0/fuzz/fuzz_milter.c
--- old/postsrsd-2.3.0/fuzz/fuzz_milter.c       1970-01-01 01:00:00.000000000 
+0100
+++ new/postsrsd-2.4.0/fuzz/fuzz_milter.c       2026-08-12 10:28:49.000000000 
+0200
@@ -0,0 +1,10 @@
+#include "fuzz.h"
+
+#include <stddef.h>
+#include <stdint.h>
+
+int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
+{
+    fuzz_client(handle_milter_client, data, size);
+    return 0;
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/fuzz_milter_parsers.c 
new/postsrsd-2.4.0/fuzz/fuzz_milter_parsers.c
--- old/postsrsd-2.3.0/fuzz/fuzz_milter_parsers.c       1970-01-01 
01:00:00.000000000 +0100
+++ new/postsrsd-2.4.0/fuzz/fuzz_milter_parsers.c       2026-08-12 
10:28:49.000000000 +0200
@@ -0,0 +1,17 @@
+#include <milter.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
+{
+    char* output;
+    output = milter_parse_macros("i", data, size);
+    if (output != NULL)
+        free(output);
+    list_t* L = list_create();
+    milter_parse_str_list(L, data, size);
+    list_destroy(L, free);
+    return 0;
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/fuzz_netstring.c 
new/postsrsd-2.4.0/fuzz/fuzz_netstring.c
--- old/postsrsd-2.3.0/fuzz/fuzz_netstring.c    1970-01-01 01:00:00.000000000 
+0100
+++ new/postsrsd-2.4.0/fuzz/fuzz_netstring.c    2026-08-12 10:28:49.000000000 
+0200
@@ -0,0 +1,24 @@
+#include <netstring.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
+{
+    char output[1024];
+    size_t decoded_length = 0;
+    netstring_decode(data, size, output, sizeof(output), &decoded_length);
+
+    int fds[2];
+    if (pipe(fds) < 0)
+        abort();
+    if (size > 4096)
+        size = 4096;
+    if (write(fds[1], data, size) != (ssize_t)size)
+        abort();
+    close(fds[1]);
+    netstring_read(fds[0], output, sizeof(output), &decoded_length);
+    close(fds[0]);
+    return 0;
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/fuzz_socketmap.c 
new/postsrsd-2.4.0/fuzz/fuzz_socketmap.c
--- old/postsrsd-2.3.0/fuzz/fuzz_socketmap.c    1970-01-01 01:00:00.000000000 
+0100
+++ new/postsrsd-2.4.0/fuzz/fuzz_socketmap.c    2026-08-12 10:28:49.000000000 
+0200
@@ -0,0 +1,10 @@
+#include "fuzz.h"
+
+#include <stddef.h>
+#include <stdint.h>
+
+int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
+{
+    fuzz_client(handle_socketmap_client, data, size);
+    return 0;
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/fuzz_srs.c 
new/postsrsd-2.4.0/fuzz/fuzz_srs.c
--- old/postsrsd-2.3.0/fuzz/fuzz_srs.c  1970-01-01 01:00:00.000000000 +0100
+++ new/postsrsd-2.4.0/fuzz/fuzz_srs.c  2026-08-12 10:28:49.000000000 +0200
@@ -0,0 +1,29 @@
+#include <srs2.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
+{
+    srs_t* srs = srs_new();
+    if (srs == NULL)
+        abort();
+    srs_add_secret(srs, "tops3cr3t");
+    srs_set_hashlength(srs, 4);
+    srs_set_hashmin(srs, 4);
+    srs->faketime = 1577836860;
+
+    char* input = malloc(size + 1);
+    if (input == NULL)
+        abort();
+    memcpy(input, data, size);
+    input[size] = 0;
+
+    char output[4096];
+    srs_reverse(srs, output, sizeof(output), input);
+
+    free(input);
+    srs_free(srs);
+    return 0;
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/seeds/gen_milter.py 
new/postsrsd-2.4.0/fuzz/seeds/gen_milter.py
--- old/postsrsd-2.3.0/fuzz/seeds/gen_milter.py 1970-01-01 01:00:00.000000000 
+0100
+++ new/postsrsd-2.4.0/fuzz/seeds/gen_milter.py 2026-08-12 10:28:49.000000000 
+0200
@@ -0,0 +1,22 @@
+#!/usr/bin/python3
+
+from pathlib import Path
+import struct
+
+DIR = Path(__file__).parent / "milter"
+
+
+def pkt(payload: bytes):
+    return struct.pack(">L", len(payload)) + payload
+
+
+optneg = struct.pack(">cLLL", b"O", 6, 0xFF, 0xFF)
+mailfrom = b"M<[email protected]>\x00"
+rcptto = b"R<[email protected]>\x00"
+eom = b"E"
+
+(DIR / "valid").write_bytes(pkt(optneg) + pkt(mailfrom) + pkt(rcptto) + 
pkt(eom))
+(DIR / "optneg").write_bytes(pkt(optneg))
+(DIR / "mailfrom").write_bytes(pkt(mailfrom))
+(DIR / "rcptto").write_bytes(pkt(rcptto))
+(DIR / "eom").write_bytes(pkt(eom))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/seeds/gen_milter_parsers.py 
new/postsrsd-2.4.0/fuzz/seeds/gen_milter_parsers.py
--- old/postsrsd-2.3.0/fuzz/seeds/gen_milter_parsers.py 1970-01-01 
01:00:00.000000000 +0100
+++ new/postsrsd-2.4.0/fuzz/seeds/gen_milter_parsers.py 2026-08-12 
10:28:49.000000000 +0200
@@ -0,0 +1,10 @@
+#!/usr/bin/python3
+
+from pathlib import Path
+import struct
+
+DIR = Path(__file__).parent / "milter_parsers"
+
+example_list = [b"i", b"foo", b"bar", b"foobar", b"[email protected]"]
+
+(DIR / "list").write_bytes(b"\x00".join(example_list))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/seeds/gen_socketmap.py 
new/postsrsd-2.4.0/fuzz/seeds/gen_socketmap.py
--- old/postsrsd-2.3.0/fuzz/seeds/gen_socketmap.py      1970-01-01 
01:00:00.000000000 +0100
+++ new/postsrsd-2.4.0/fuzz/seeds/gen_socketmap.py      2026-08-12 
10:28:49.000000000 +0200
@@ -0,0 +1,15 @@
+#!/usr/bin/python3
+
+from pathlib import Path
+
+SEEDS = {
+    "forward": "forward [email protected]",
+    "reverse": "reverse [email protected]",
+}
+
+DIR = Path(__file__).parent / "socketmap"
+
+DIR.mkdir(exist_ok=True)
+for seed_file, value in SEEDS.items():
+    with open(DIR / seed_file, "w") as f:
+        f.write(f"{len(value)}:{value},")
Binary files old/postsrsd-2.3.0/fuzz/seeds/milter/eom and 
new/postsrsd-2.4.0/fuzz/seeds/milter/eom differ
Binary files old/postsrsd-2.3.0/fuzz/seeds/milter/mailfrom and 
new/postsrsd-2.4.0/fuzz/seeds/milter/mailfrom differ
Binary files old/postsrsd-2.3.0/fuzz/seeds/milter/optneg and 
new/postsrsd-2.4.0/fuzz/seeds/milter/optneg differ
Binary files old/postsrsd-2.3.0/fuzz/seeds/milter/rcptto and 
new/postsrsd-2.4.0/fuzz/seeds/milter/rcptto differ
Binary files old/postsrsd-2.3.0/fuzz/seeds/milter/valid and 
new/postsrsd-2.4.0/fuzz/seeds/milter/valid differ
Binary files old/postsrsd-2.3.0/fuzz/seeds/milter_parsers/list and 
new/postsrsd-2.4.0/fuzz/seeds/milter_parsers/list differ
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/seeds/netstring/a 
new/postsrsd-2.4.0/fuzz/seeds/netstring/a
--- old/postsrsd-2.3.0/fuzz/seeds/netstring/a   1970-01-01 01:00:00.000000000 
+0100
+++ new/postsrsd-2.4.0/fuzz/seeds/netstring/a   2026-08-12 10:28:49.000000000 
+0200
@@ -0,0 +1,2 @@
+1:a,
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/seeds/netstring/empty 
new/postsrsd-2.4.0/fuzz/seeds/netstring/empty
--- old/postsrsd-2.3.0/fuzz/seeds/netstring/empty       1970-01-01 
01:00:00.000000000 +0100
+++ new/postsrsd-2.4.0/fuzz/seeds/netstring/empty       2026-08-12 
10:28:49.000000000 +0200
@@ -0,0 +1 @@
+0:,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/seeds/netstring/forward_foobar 
new/postsrsd-2.4.0/fuzz/seeds/netstring/forward_foobar
--- old/postsrsd-2.3.0/fuzz/seeds/netstring/forward_foobar      1970-01-01 
01:00:00.000000000 +0100
+++ new/postsrsd-2.4.0/fuzz/seeds/netstring/forward_foobar      2026-08-12 
10:28:49.000000000 +0200
@@ -0,0 +1 @@
+17:forward foo@bar,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/seeds/netstring/test 
new/postsrsd-2.4.0/fuzz/seeds/netstring/test
--- old/postsrsd-2.3.0/fuzz/seeds/netstring/test        1970-01-01 
01:00:00.000000000 +0100
+++ new/postsrsd-2.4.0/fuzz/seeds/netstring/test        2026-08-12 
10:28:49.000000000 +0200
@@ -0,0 +1,2 @@
+4:test,
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/seeds/socketmap/forward 
new/postsrsd-2.4.0/fuzz/seeds/socketmap/forward
--- old/postsrsd-2.3.0/fuzz/seeds/socketmap/forward     1970-01-01 
01:00:00.000000000 +0100
+++ new/postsrsd-2.4.0/fuzz/seeds/socketmap/forward     2026-08-12 
10:28:49.000000000 +0200
@@ -0,0 +1 @@
+28:forward [email protected],
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/seeds/socketmap/reverse 
new/postsrsd-2.4.0/fuzz/seeds/socketmap/reverse
--- old/postsrsd-2.3.0/fuzz/seeds/socketmap/reverse     1970-01-01 
01:00:00.000000000 +0100
+++ new/postsrsd-2.4.0/fuzz/seeds/socketmap/reverse     2026-08-12 
10:28:49.000000000 +0200
@@ -0,0 +1 @@
+53:reverse [email protected],
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/seeds/srs/malformed1 
new/postsrsd-2.4.0/fuzz/seeds/srs/malformed1
--- old/postsrsd-2.3.0/fuzz/seeds/srs/malformed1        1970-01-01 
01:00:00.000000000 +0100
+++ new/postsrsd-2.4.0/fuzz/seeds/srs/malformed1        2026-08-12 
10:28:49.000000000 +0200
@@ -0,0 +1 @@
+SRS0=
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/seeds/srs/malformed2 
new/postsrsd-2.4.0/fuzz/seeds/srs/malformed2
--- old/postsrsd-2.3.0/fuzz/seeds/srs/malformed2        1970-01-01 
01:00:00.000000000 +0100
+++ new/postsrsd-2.4.0/fuzz/seeds/srs/malformed2        2026-08-12 
10:28:49.000000000 +0200
@@ -0,0 +1 @@
+SRS0=AAAA=
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/seeds/srs/malformed3 
new/postsrsd-2.4.0/fuzz/seeds/srs/malformed3
--- old/postsrsd-2.3.0/fuzz/seeds/srs/malformed3        1970-01-01 
01:00:00.000000000 +0100
+++ new/postsrsd-2.4.0/fuzz/seeds/srs/malformed3        2026-08-12 
10:28:49.000000000 +0200
@@ -0,0 +1 @@
+SRS0=AAAA=AA=
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/seeds/srs/plain 
new/postsrsd-2.4.0/fuzz/seeds/srs/plain
--- old/postsrsd-2.3.0/fuzz/seeds/srs/plain     1970-01-01 01:00:00.000000000 
+0100
+++ new/postsrsd-2.4.0/fuzz/seeds/srs/plain     2026-08-12 10:28:49.000000000 
+0200
@@ -0,0 +1 @@
[email protected]
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/fuzz/seeds/srs/valid 
new/postsrsd-2.4.0/fuzz/seeds/srs/valid
--- old/postsrsd-2.3.0/fuzz/seeds/srs/valid     1970-01-01 01:00:00.000000000 
+0100
+++ new/postsrsd-2.4.0/fuzz/seeds/srs/valid     2026-08-12 10:28:49.000000000 
+0200
@@ -0,0 +1 @@
[email protected]
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/src/config.c 
new/postsrsd-2.4.0/src/config.c
--- old/postsrsd-2.3.0/src/config.c     2026-07-17 19:04:57.000000000 +0200
+++ new/postsrsd-2.4.0/src/config.c     2026-08-12 10:28:49.000000000 +0200
@@ -68,6 +68,18 @@
     return 0;
 }
 
+static int validate_hash_size(cfg_t* cfg, cfg_opt_t* opt)
+{
+    int value = cfg_opt_getnint(opt, cfg_opt_size(opt) - 1);
+    if (value < 1 || value > 20)
+    {
+        cfg_error(cfg, "option '%s' must be between 1 and 20 inclusive.",
+                  cfg_opt_name(opt));
+        return -1;
+    }
+    return 0;
+}
+
 static bool is_valid_domain_name(const char* s)
 {
     char prev = 0;
@@ -79,7 +91,7 @@
     {
         if (*s == '.' && prev == '.')
             return false;
-        if (!isalnum(*s) && *s != '-' && *s != '.')
+        if (!isalnum((unsigned char)*s) && *s != '-' && *s != '.')
             return false;
         prev = *s++;
     }
@@ -170,8 +182,10 @@
         CFG_BOOL("always-rewrite", cfg_false, CFGF_NONE),
         CFG_STR("socketmap", "unix:/var/spool/postfix/srs", CFGF_NONE),
         CFG_INT("keep-alive", 30, CFGF_NONE),
+        CFG_INT("connection-limit", 200, CFGF_NONE),
         CFG_STR("milter", NULL, CFGF_NODEFAULT),
         CFG_BOOL("milter-rewrite-local", cfg_false, CFGF_NONE),
+        CFG_INT("milter-recipient-limit", 1000, CFGF_NONE),
         CFG_STR("secrets-file", DEFAULT_SECRETS_FILE, CFGF_NONE),
         CFG_STR("envelope-database", NULL, CFGF_NODEFAULT),
         CFG_STR("pid-file", NULL, CFGF_NODEFAULT),
@@ -187,7 +201,11 @@
     cfg_set_validate_func(cfg, "separator", validate_separator);
     cfg_set_validate_func(cfg, "srs-domain", validate_domain_names);
     cfg_set_validate_func(cfg, "domains", validate_domain_names);
+    cfg_set_validate_func(cfg, "hash-length", validate_hash_size);
+    cfg_set_validate_func(cfg, "hash-minimum", validate_hash_size);
     cfg_set_validate_func(cfg, "keep-alive", validate_uint);
+    cfg_set_validate_func(cfg, "connection-limit", validate_uint);
+    cfg_set_validate_func(cfg, "milter-recipient-limit", validate_uint);
     cfg_set_validate_func(cfg, "unprivileged-user", 
validate_unprivileged_user);
     return cfg;
 }
@@ -280,10 +298,18 @@
 
 srs_t* srs_from_config(cfg_t* cfg)
 {
+    int hash_length = cfg_getint(cfg, "hash-length");
+    int hash_minimum = cfg_getint(cfg, "hash-minimum");
+    if (hash_minimum > hash_length)
+    {
+        log_error("hash-minimum (%d) exceeds hash-length (%d)", hash_minimum,
+                  hash_length);
+        return NULL;
+    }
     srs_t* srs = srs_new();
     srs_set_alwaysrewrite(srs, cfg_getbool(cfg, "always-rewrite"));
-    srs_set_hashlength(srs, cfg_getint(cfg, "hash-length"));
-    srs_set_hashmin(srs, cfg_getint(cfg, "hash-minimum"));
+    srs_set_hashlength(srs, hash_length);
+    srs_set_hashmin(srs, hash_minimum);
     srs_set_separator(srs, cfg_getstr(cfg, "separator")[0]);
     char* secrets_file = cfg_getstr(cfg, "secrets-file");
     if (NONEMPTY_STRING(secrets_file))
@@ -375,9 +401,9 @@
                     *end = 0;
                 else
                     end = domain + strlen(domain);
-                while (isspace(domain[0]))
+                while (isspace((unsigned char)domain[0]))
                     ++domain;
-                while (end != domain && isspace(*(end - 1)))
+                while (end != domain && isspace((unsigned char)*(end - 1)))
                     *--end = 0;
                 if (NULL_OR_EMPTY_STRING(domain))
                     continue;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/src/database.c 
new/postsrsd-2.4.0/src/database.c
--- old/postsrsd-2.3.0/src/database.c   2026-07-17 19:04:57.000000000 +0200
+++ new/postsrsd-2.4.0/src/database.c   2026-08-12 10:28:49.000000000 +0200
@@ -70,7 +70,9 @@
     }
     if (result == SQLITE_ROW)
     {
-        value = strdup((const char*)sqlite3_column_text(db->read_stmt, 0));
+        const char* column_text =
+            (const char*)sqlite3_column_text(db->read_stmt, 0);
+        value = NONEMPTY_STRING(column_text) ? strdup(column_text) : NULL;
     }
     sqlite3_reset(db->read_stmt);
     sqlite3_clear_bindings(db->read_stmt);
@@ -195,7 +197,7 @@
     }
     if (reply->type == REDIS_REPLY_STRING)
     {
-        value = strdup(reply->str);
+        value = NONEMPTY_STRING(reply->str) ? strdup(reply->str) : NULL;
     }
     freeReplyObject(reply);
     return value;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/src/endpoint.c 
new/postsrsd-2.4.0/src/endpoint.c
--- old/postsrsd-2.3.0/src/endpoint.c   2026-07-17 19:04:57.000000000 +0200
+++ new/postsrsd-2.4.0/src/endpoint.c   2026-08-12 10:28:49.000000000 +0200
@@ -97,6 +97,7 @@
     endpoint->fd[endpoint->num_fds++] = sock;
     if (endpoint->lock >= 0)
         endpoint->path = strdup(path);
+    umask(old_mask);
     return true;
 fail:
     umask(old_mask);
@@ -177,7 +178,7 @@
     if (err != 0)
     {
         log_error("%s", gai_strerror(err));
-        return -1;
+        return false;
     }
     int sock = -1, count = 0, free_fds = POSTSRSD_MAX_FDS - endpoint->num_fds;
     for (struct addrinfo* it = ai; it; it = it->ai_next)
@@ -212,8 +213,8 @@
     }
     freeaddrinfo(ai);
     if (count == 0 && err != 0)
-        return -1;
-    return count;
+        return false;
+    return true;
 }
 
 endpoint_t* endpoint_create(const char* s)
@@ -225,7 +226,7 @@
         return NULL;
     }
     result->num_fds = 0;
-    for (unsigned i = 0; i < result->num_fds; ++i)
+    for (unsigned i = 0; i < POSTSRSD_MAX_FDS; ++i)
         result->fd[i] = -1;
     result->lock = -1;
     result->path = NULL;
@@ -243,6 +244,7 @@
         if (create_unix_socket(path, result))
             return result;
         log_error("failed to create endpoint '%s'", s);
+
         endpoint_destroy(result);
         return NULL;
     }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/src/main.c 
new/postsrsd-2.4.0/src/main.c
--- old/postsrsd-2.3.0/src/main.c       2026-07-17 19:04:57.000000000 +0200
+++ new/postsrsd-2.4.0/src/main.c       2026-08-12 10:28:49.000000000 +0200
@@ -14,14 +14,13 @@
  * You should have received a copy of the GNU General Public License
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
-#include "config.h"
+#include "main.h"
+
 #include "database.h"
-#include "endpoint.h"
 #include "milter.h"
 #include "netstring.h"
 #include "postsrsd_build_config.h"
 #include "srs.h"
-#include "util.h"
 
 #include <errno.h>
 #include <fcntl.h>
@@ -49,20 +48,7 @@
 static bool sd_notify_support = false;
 static sandbox_t* sandbox = NULL;
 
-struct postsrsd
-{
-    cfg_t* cfg;
-    srs_t* srs;
-    endpoint_t* socketmap;
-    endpoint_t* milter;
-    char* srs_domain;
-    domain_set_t* local_domains;
-    file_watch_t* file_watch;
-    int target_uid, target_gid;
-};
-typedef struct postsrsd postsrsd_t;
-
-static void init_state(postsrsd_t* state)
+void init_state(postsrsd_t* state)
 {
     state->cfg = NULL;
     state->srs = NULL;
@@ -73,9 +59,10 @@
     state->file_watch = NULL;
     state->target_uid = 0;
     state->target_gid = 0;
+    state->connection_limit = 0;
 }
 
-static void finalize_state(postsrsd_t* state)
+void finalize_state(postsrsd_t* state)
 {
     if (state->file_watch != NULL)
     {
@@ -270,7 +257,7 @@
     return true;
 }
 
-static void handle_socketmap_client(postsrsd_t* state, int conn)
+void handle_socketmap_client(postsrsd_t* state, int conn)
 {
     database_t* db;
     if (!prepare_client(state, conn, &db))
@@ -358,7 +345,7 @@
     database_disconnect(db);
 }
 
-static void handle_milter_client(postsrsd_t* state, int conn)
+void handle_milter_client(postsrsd_t* state, int conn)
 {
 #define MILTER_AWAIT_OPTNEG      0
 #define MILTER_AWAIT_MAIL        1
@@ -373,6 +360,8 @@
         return;
     const bool always_rewrite = cfg_getbool(state->cfg, "always-rewrite");
     const bool rewrite_local = cfg_getbool(state->cfg, "milter-rewrite-local");
+    const size_t milter_recipient_limit =
+        cfg_getint(state->cfg, "milter-recipient-limit");
     const int keep_alive = cfg_getint(state->cfg, "keep-alive");
     int milter_state = MILTER_AWAIT_OPTNEG;
     char* queue_id = NULL;
@@ -474,6 +463,16 @@
                         goto done;
                     goto cleanup;
                 }
+                if (list_size(recipients) >= milter_recipient_limit)
+                {
+                    log_error(
+                        "%s: MTA exceeded the recipient limit per mail "
+                        "transaction",
+                        queue_id);
+                    if (!milter_reject(conn))
+                        goto done;
+                    goto cleanup;
+                }
                 list_append(recipients, strndup(buffer + 1, len - 1));
                 if (!milter_continue(conn))
                     goto done;
@@ -727,6 +726,7 @@
     }
     /* If we reached this point, the new configuration is valid, so we commit 
*/
     finalize_state(state);
+    new_state.connection_limit = cfg_getint(new_state.cfg, "connection-limit");
     *state = new_state;
     return true;
 fail:
@@ -817,6 +817,7 @@
     } while (pid > 0);
 }
 
+#ifndef POSTSRSD_FUZZING
 int main(int argc, char** argv)
 {
     postsrsd_t state;
@@ -824,12 +825,12 @@
     FILE* pf = NULL;
     pid_set_t* P = NULL;
     int exit_code = EXIT_FAILURE;
-#ifdef HAVE_CLOSE_RANGE
+#    ifdef HAVE_CLOSE_RANGE
     close_range(3, ~0U, 0);
-#else
+#    else
     for (int fd = 3; fd < 1024; ++fd)
         close(fd);
-#endif
+#    endif
     if (!setup_state(argc, argv, &state))
         goto shutdown;
     sandbox = sandbox_init();
@@ -942,6 +943,12 @@
                         log_perror(errno, "accept");
                         continue;
                     }
+                    if (pid_set_size(P) >= state.connection_limit)
+                    {
+                        log_warn("connection limit reached");
+                        close(conn);
+                        continue;
+                    }
                     pid_t pid = fork();
                     if (pid == 0)
                     {
@@ -991,3 +998,4 @@
     pid_set_destroy(P);
     return exit_code;
 }
+#endif
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/src/main.h 
new/postsrsd-2.4.0/src/main.h
--- old/postsrsd-2.3.0/src/main.h       1970-01-01 01:00:00.000000000 +0100
+++ new/postsrsd-2.4.0/src/main.h       2026-08-12 10:28:49.000000000 +0200
@@ -0,0 +1,45 @@
+/* PostSRSd - Sender Rewriting Scheme daemon for Postfix
+ * Copyright 2012-2026 Timo Röhling <[email protected]>
+ * SPDX-License-Identifier: GPL-3.0-only
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+#ifndef MAIN_H
+#define MAIN_H
+
+#include "config.h"
+#include "endpoint.h"
+#include "srs2.h"
+#include "util.h"
+
+struct postsrsd
+{
+    cfg_t* cfg;
+    srs_t* srs;
+    endpoint_t* socketmap;
+    endpoint_t* milter;
+    char* srs_domain;
+    domain_set_t* local_domains;
+    file_watch_t* file_watch;
+    int target_uid, target_gid;
+    size_t connection_limit;
+};
+typedef struct postsrsd postsrsd_t;
+
+void init_state(postsrsd_t* state);
+void finalize_state(postsrsd_t* state);
+
+void handle_socketmap_client(postsrsd_t* state, int conn);
+void handle_milter_client(postsrsd_t* state, int conn);
+
+#endif
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/src/milter.c 
new/postsrsd-2.4.0/src/milter.c
--- old/postsrsd-2.3.0/src/milter.c     2026-07-17 19:04:57.000000000 +0200
+++ new/postsrsd-2.4.0/src/milter.c     2026-08-12 10:28:49.000000000 +0200
@@ -62,14 +62,11 @@
     len -= read_len;
     while (len > 0)
     {
-        read_len = len < sizeof(discardpile) ? len : sizeof(discardpile);
-        r = read(fd, discardpile, read_len);
+        size_t discard_len =
+            len < sizeof(discardpile) ? len : sizeof(discardpile);
+        r = read(fd, discardpile, discard_len);
         if (r <= 0)
-        {
-            if (r < 0 && errno == EINTR)
-                continue;
             break;
-        }
         len -= r;
         if (truncated != NULL)
             *truncated += r;
@@ -137,6 +134,8 @@
     size_t numv = list_size(L);
     if (numv == 0)
         return milter_send(fd, action);
+    if (numv > 127)
+        return false;
     struct iovec iov[numv + 1];
     size_t j = 1, length = 0;
     for (size_t i = 0; i < numv; ++i)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/src/netstring.c 
new/postsrsd-2.4.0/src/netstring.c
--- old/postsrsd-2.3.0/src/netstring.c  2026-07-17 19:04:57.000000000 +0200
+++ new/postsrsd-2.4.0/src/netstring.c  2026-08-12 10:28:49.000000000 +0200
@@ -28,29 +28,36 @@
     if (data == NULL)
         return NULL;
     int i = snprintf(buffer, bufsize, "%zu:", length);
-    if (i <= 0 || length >= bufsize - i)
+    if (i <= 0 || (size_t)i >= bufsize || length >= bufsize - i)
         return NULL;
-    strncpy(&buffer[i], data, length);
+    memcpy(&buffer[i], data, length);
     buffer[length + i] = ',';
     if (encoded_length != NULL)
         *encoded_length = length + i + 1;
     return buffer;
 }
 
-char* netstring_decode(const char* netstring, char* buffer, size_t bufsize,
-                       size_t* decoded_length)
+char* netstring_decode(const char* netstring, size_t encoded_length,
+                       char* buffer, size_t bufsize, size_t* decoded_length)
 {
     if (netstring == NULL)
         return NULL;
-    int i = -1;
-    size_t length;
-    if (sscanf(netstring, "%5zu%n", &length, &i) < 1)
-        return NULL;
-    if (i < 0 || length >= bufsize)
+    size_t i = 0;
+    size_t length = 0;
+    while (i < encoded_length && netstring[i] != ':')
+    {
+        if (netstring[i] < '0' || netstring[i] > '9')
+            return NULL;
+        length = 10 * length + (netstring[i] - '0');
+        if (length > 100000 || length >= bufsize)
+            return NULL;
+        ++i;
+    }
+    if (i == 0 || length >= bufsize || length + i + 1 >= encoded_length)
         return NULL;
     if (netstring[i] != ':' || netstring[length + i + 1] != ',')
         return NULL;
-    strncpy(buffer, &netstring[i + 1], length);
+    memcpy(buffer, &netstring[i + 1], length);
     if (decoded_length != NULL)
         *decoded_length = length;
     buffer[length] = 0;
@@ -110,6 +117,8 @@
         if (data == NULL)
             return -1;
         size_t n = snprintf(prefix, sizeof(prefix), "%zu:", length);
+        if (n >= sizeof(prefix))
+            return -1;
         iov[0].iov_base = (void*)prefix;
         iov[0].iov_len = n;
         iov[1].iov_base = (void*)data;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/src/netstring.h 
new/postsrsd-2.4.0/src/netstring.h
--- old/postsrsd-2.3.0/src/netstring.h  2026-07-17 19:04:57.000000000 +0200
+++ new/postsrsd-2.4.0/src/netstring.h  2026-08-12 10:28:49.000000000 +0200
@@ -22,8 +22,8 @@
 
 char* netstring_encode(const char* data, size_t length, char* buffer,
                        size_t bufsize, size_t* encoded_length);
-char* netstring_decode(const char* netstring, char* buffer, size_t bufsize,
-                       size_t* decoded_length);
+char* netstring_decode(const char* netstring, size_t encoded_length,
+                       char* buffer, size_t bufsize, size_t* decoded_length);
 char* netstring_read(int fd, char* buffer, size_t bufsize,
                      size_t* decoded_length);
 int netstring_write(int fd, const char* data, size_t length);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/src/srs.c new/postsrsd-2.4.0/src/srs.c
--- old/postsrsd-2.3.0/src/srs.c        2026-07-17 19:04:57.000000000 +0200
+++ new/postsrsd-2.4.0/src/srs.c        2026-08-12 10:28:49.000000000 +0200
@@ -149,7 +149,7 @@
     {
         if (db != NULL)
         {
-            char* p = buffer;
+            unsigned char* p = (unsigned char*)buffer;
             while (*p)
             {
                 *p = toupper(*p);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/src/srs2.c 
new/postsrsd-2.4.0/src/srs2.c
--- old/postsrsd-2.3.0/src/srs2.c       2026-07-17 19:04:57.000000000 +0200
+++ new/postsrsd-2.4.0/src/srs2.c       2026-08-12 10:28:49.000000000 +0200
@@ -34,6 +34,30 @@
 static srs_realloc_t srs_f_realloc = realloc;
 static srs_free_t srs_f_free = free;
 
+int base64_equivalent(const char* s1, const char* s2, size_t length)
+{
+    static const int TRANSLATION_START = 43;
+    static const char TRANSLATION[] = {
+        '-', 0,   '-', 0,   '_', '0', '1', '2', '3', '4', '5', '6', '7', '8',
+        '9', 0,   0,   0,   0,   0,   0,   0,   'a', 'b', 'c', 'd', 'e', 'f',
+        'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
+        'u', 'v', 'w', 'x', 'y', 'z', 0,   0,   0,   0,   '_', 0,   'a', 'b',
+        'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
+        'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};
+    int comp = 0;
+    for (size_t i = 0; i < length; ++i)
+    {
+        int h1 = (int)s1[i] - TRANSLATION_START;
+        int h2 = (int)s2[i] - TRANSLATION_START;
+        int v1 =
+            (h1 >= 0 && (size_t)h1 < sizeof(TRANSLATION)) ? TRANSLATION[h1] : 
0;
+        int v2 =
+            (h2 >= 0 && (size_t)h2 < sizeof(TRANSLATION)) ? TRANSLATION[h2] : 
0;
+        comp |= (v1 ^ v2) | (v1 == 0) | (v2 == 0);
+    }
+    return comp == 0;
+}
+
 int srs_set_malloc(srs_malloc_t m, srs_realloc_t r, srs_free_t f)
 {
     srs_f_malloc = m;
@@ -212,7 +236,7 @@
     then = 0;
     for (sp = stamp; *sp; sp++)
     {
-        bp = strchr(SRS_TIME_BASECHARS, toupper(*sp));
+        bp = strchr(SRS_TIME_BASECHARS, toupper((unsigned char)*sp));
         if (bp == NULL)
             return SRS_EBADTIMESTAMPCHAR;
         off = bp - SRS_TIME_BASECHARS;
@@ -272,8 +296,8 @@
         char lcdata[len + 1];
         for (j = 0; j < len; j++)
         {
-            if (isupper(data[j]))
-                lcdata[j] = tolower(data[j]);
+            if (isupper((unsigned char)data[j]))
+                lcdata[j] = tolower((unsigned char)data[j]);
             else
                 lcdata[j] = data[j];
         }
@@ -345,7 +369,7 @@
 {
     va_list ap;
     int len;
-    int i, j;
+    int i;
 
     len = strlen(hash);
     if (len < srs->hashmin)
@@ -361,16 +385,7 @@
         va_start(ap, nargs);
         srs_hash_create_v(srs, i, srshash, nargs, ap);
         va_end(ap);
-        if (strncasecmp(hash, srshash, len) == 0)
-            return SRS_SUCCESS;
-        for (j = 0; j < len; ++j)
-        {
-            if (srshash[j] == '-')
-                srshash[j] = '+';
-            if (srshash[j] == '_')
-                srshash[j] = '/';
-        }
-        if (strncasecmp(hash, srshash, len) == 0)
+        if (base64_equivalent(hash, srshash, len))
             return SRS_SUCCESS;
     }
 
@@ -619,7 +634,9 @@
 int srs_reverse(srs_t* srs, char* buf, unsigned buflen, const char* sender)
 {
     char* tmp;
-    unsigned len;
+    unsigned len = strlen(sender);
+    if (len < 5)
+        return SRS_ENOTSRSADDRESS;
 
     if (!SRS_IS_SRS_ADDRESS(sender))
         return SRS_ENOTSRSADDRESS;
@@ -627,7 +644,6 @@
     if (srs->noreverse)
         return SRS_ENOTREWRITTEN;
 
-    len = strlen(sender);
     if (len >= buflen)
         return SRS_EBUFTOOSMALL;
     char senduser[len + 1];
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/src/util.c 
new/postsrsd-2.4.0/src/util.c
--- old/postsrsd-2.3.0/src/util.c       2026-07-17 19:04:57.000000000 +0200
+++ new/postsrsd-2.4.0/src/util.c       2026-08-12 10:28:49.000000000 +0200
@@ -207,11 +207,7 @@
     {
         ssize_t r = read(fd, buffer + total, size - total);
         if (r <= 0)
-        {
-            if (r < 0 && errno == EINTR)
-                continue;
             return false;
-        }
         total += r;
     }
     return true;
@@ -223,9 +219,7 @@
     do
     {
         ssize_t written = writev(fd, iov + i, numv - i);
-        if (written < 0 && errno != EINTR)
-            return false;
-        if (written == 0)
+        if (written <= 0)
             return false;
         while (written > 0)
         {
@@ -369,6 +363,11 @@
     return P;
 }
 
+size_t pid_set_size(pid_set_t* P)
+{
+    return P != NULL ? P->size : 0;
+}
+
 bool pid_set_add(pid_set_t* P, pid_t pid)
 {
     if (P == NULL)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/src/util.h 
new/postsrsd-2.4.0/src/util.h
--- old/postsrsd-2.3.0/src/util.h       2026-07-17 19:04:57.000000000 +0200
+++ new/postsrsd-2.4.0/src/util.h       2026-08-12 10:28:49.000000000 +0200
@@ -73,6 +73,7 @@
 void domain_set_destroy(domain_set_t* D);
 
 pid_set_t* pid_set_create();
+size_t pid_set_size(pid_set_t* P);
 bool pid_set_add(pid_set_t* P, pid_t pid);
 bool pid_set_remove(pid_set_t* P, pid_t pid);
 bool pid_set_kill(pid_set_t* P, int signal);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/tests/blackbox/milter.py 
new/postsrsd-2.4.0/tests/blackbox/milter.py
--- old/postsrsd-2.3.0/tests/blackbox/milter.py 2026-07-17 19:04:57.000000000 
+0200
+++ new/postsrsd-2.4.0/tests/blackbox/milter.py 2026-08-12 10:28:49.000000000 
+0200
@@ -233,6 +233,16 @@
         code = milter_read(sock_stream)[:1]
         assert code == b"r", "milter should have rejected"
 
+    def too_many_recipients(sock_stream: SockStream):
+        assert mf_optneg(sock_stream), "milter option negotiation failed"
+        assert mf_envfrom(sock_stream, "a" * 508), "milter MAIL command failed"
+        code = None
+        for number in range(10):
+            code = mf_rcptto(sock_stream, f"recipient{number}@example.com")
+            if code == b"r":
+                break
+        assert code == b"r", "milter should have rejected"
+
     def malformed_rcpt_command(sock_stream: SockStream):
         assert mf_optneg(sock_stream), "milter option negotiation failed"
         assert mf_envfrom(sock_stream, "a" * 508), "milter MAIL command failed"
@@ -282,6 +292,7 @@
             malformed_mail_command,
             oversized_rcpt_command,
             malformed_rcpt_command,
+            too_many_recipients,
             missing_null_terminators,
             aborted_transaction,
             close_on_quit,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/tests/blackbox/testhelper.py 
new/postsrsd-2.4.0/tests/blackbox/testhelper.py
--- old/postsrsd-2.3.0/tests/blackbox/testhelper.py     2026-07-17 
19:04:57.000000000 +0200
+++ new/postsrsd-2.4.0/tests/blackbox/testhelper.py     2026-08-12 
10:28:49.000000000 +0200
@@ -145,6 +145,7 @@
                     f'domains-file = "{self._tmpdir_path / 
"postsrsd.domains"}"\n'
                     f'domains-file-watch = {"on" if use_file_watch else 
"off"}\n'
                     "keep-alive = 1\n"
+                    "milter-recipient-limit = 5\n"
                     'chroot-dir = ""\n'
                     'unprivileged-user = ""\n'
                     f'original-envelope = {"embedded" if database == 
Database.NONE else "database"}\n'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/postsrsd-2.3.0/tests/unit/test_netstring.c 
new/postsrsd-2.4.0/tests/unit/test_netstring.c
--- old/postsrsd-2.3.0/tests/unit/test_netstring.c      2026-07-17 
19:04:57.000000000 +0200
+++ new/postsrsd-2.4.0/tests/unit/test_netstring.c      2026-08-12 
10:28:49.000000000 +0200
@@ -58,34 +58,38 @@
     char* result;
     size_t length;
 
-    result = netstring_decode("8:PostSRSd,", buffer, sizeof(buffer), &length);
+    result =
+        netstring_decode("8:PostSRSd,", 12, buffer, sizeof(buffer), &length);
     ck_assert_ptr_nonnull(result);
     ck_assert_uint_eq(length, 8);
     ck_assert_mem_eq(result, "PostSRSd", length);
 
-    result = netstring_decode("16:0123456789abcdef,", buffer, sizeof(buffer),
-                              &length);
+    result = netstring_decode("16:0123456789abcdef,", 21, buffer,
+                              sizeof(buffer), &length);
     ck_assert_ptr_nonnull(result);
     ck_assert_uint_eq(length, 16);
     ck_assert_mem_eq(result, "0123456789abcdef", length);
 
-    result = netstring_decode("0:,", buffer, sizeof(buffer), &length);
+    result = netstring_decode("0:,", 4, buffer, sizeof(buffer), &length);
     ck_assert_ptr_nonnull(result);
     ck_assert_uint_eq(length, 0);
 
-    result = netstring_decode(NULL, buffer, sizeof(buffer), &length);
+    result = netstring_decode(NULL, 0, buffer, sizeof(buffer), &length);
     ck_assert_ptr_null(result);
 
-    result = netstring_decode("1a,", buffer, sizeof(buffer), &length);
+    result = netstring_decode("1a,", 4, buffer, sizeof(buffer), &length);
     ck_assert_ptr_null(result);
 
-    result = netstring_decode("1:a*", buffer, sizeof(buffer), &length);
+    result = netstring_decode("1:a*", 5, buffer, sizeof(buffer), &length);
     ck_assert_ptr_null(result);
 
-    result = netstring_decode("0x1:a,", buffer, sizeof(buffer), &length);
+    result = netstring_decode("0x1:a,", 7, buffer, sizeof(buffer), &length);
     ck_assert_ptr_null(result);
 
-    result = netstring_decode("000001:a,", buffer, sizeof(buffer), &length);
+    result = netstring_decode("2:,", 3, buffer, sizeof(buffer), &length);
+    ck_assert_ptr_null(result);
+
+    result = netstring_decode("0:", 2, buffer, sizeof(buffer), &length);
     ck_assert_ptr_null(result);
 }
 END_TEST

++++++ _scmsync.obsinfo ++++++
--- /var/tmp/diff_new_pack.81HPbR/_old  2026-08-14 22:07:24.012920617 +0200
+++ /var/tmp/diff_new_pack.81HPbR/_new  2026-08-14 22:07:24.028921199 +0200
@@ -1,5 +1,5 @@
-mtime: 1784308176
-commit: c5fb9d5d524b925af53e1ad74e7e62a0a7a687d8079a831d45e1cb88d6a93641
+mtime: 1786619326
+commit: 7dcedaecf537a9fa2bdc5d222710a54b8c9ecca744f54afe206dc5e86992ca2d
 url: https://src.opensuse.org/jengelh/postsrsd
 revision: master
 

++++++ build.specials.obscpio ++++++

++++++ build.specials.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/.gitignore new/.gitignore
--- old/.gitignore      1970-01-01 01:00:00.000000000 +0100
+++ new/.gitignore      2026-08-13 13:08:46.000000000 +0200
@@ -0,0 +1 @@
+.osc

Reply via email to