├── .clang-format ├── .cmake-format.json ├── .github └── workflows │ ├── bsd.yml │ ├── ci.yml │ └── release.yml ├── .pre-commit-config.yaml ├── CHANGELOG.rst ├── CMakeLists.txt ├── LICENSES ├── BSD-3-Clause.txt ├── FSFAP.txt ├── FSFUL.txt └── GPL-3.0-only.txt ├── README.rst ├── REUSE.toml ├── cmake ├── FindCheck.cmake ├── FindConfuse.cmake ├── FindHiredis.cmake ├── FindLibMilter.cmake ├── Findsqlite3.cmake └── utils.cmake ├── data ├── postsrsd.conf.in ├── postsrsd.service.in └── sysusers.conf.in ├── doc ├── packaging.rst └── postsrsd.conf ├── src ├── config.c ├── config.h ├── database.c ├── database.h ├── endpoint.c ├── endpoint.h ├── main.c ├── milter.c ├── milter.h ├── netstring.c ├── netstring.h ├── postsrsd_build_config.h.in ├── sha1.c ├── sha1.h ├── srs.c ├── srs.h ├── srs2.c ├── srs2.h ├── util.c └── util.h └── tests ├── CMakeLists.txt ├── blackbox ├── CMakeLists.txt ├── milter.py └── socketmap.py └── unit ├── CMakeLists.txt ├── common.h ├── test_config.c ├── test_database.c ├── test_netstring.c ├── test_sha1.c ├── test_srs2.c └── test_util.c /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: LLVM 2 | AccessModifierOffset: -4 3 | AlignConsecutiveMacros: true 4 | AlignEscapedNewlines: Left 5 | AllowShortBlocksOnASingleLine: Empty 6 | AllowShortEnumsOnASingleLine: false 7 | AllowShortFunctionsOnASingleLine: Empty 8 | AllowShortLambdasOnASingleLine: Inline 9 | AlwaysBreakBeforeMultilineStrings: true 10 | AlwaysBreakTemplateDeclarations: Yes 11 | BreakBeforeBinaryOperators: NonAssignment 12 | BreakBeforeBraces: Allman 13 | IncludeBlocks: Regroup 14 | IncludeCategories: 15 | - Regex: '<[[:alnum:]_]+>' 16 | SortPriority: 3 17 | - Regex: '<.*>' 18 | SortPriority: 2 19 | - Regex: '".*"' 20 | SortPriority: 1 21 | IndentCaseLabels: true 22 | IndentExternBlock: NoIndent 23 | IndentGotoLabels: false 24 | IndentPPDirectives: AfterHash 25 | IndentWidth: 4 26 | JavaScriptQuotes: Double 27 | PointerAlignment: Left 28 | SpacesBeforeTrailingComments: 2 29 | SpaceAfterTemplateKeyword: false 30 | -------------------------------------------------------------------------------- /.cmake-format.json: -------------------------------------------------------------------------------- 1 | { 2 | "parse": { 3 | "additional_commands": { 4 | "add_autotools_dependency": { 5 | "flags": [ 6 | ], 7 | "kwargs": { 8 | "LIBRARY_NAME": "?", 9 | "EXPORTED_TARGET": "?" 10 | } 11 | } 12 | } 13 | }, 14 | "format": { 15 | "disable": false, 16 | "line_width": 80, 17 | "tab_size": 4, 18 | "use_tabchars": false, 19 | "fractional_tab_policy": "use-space", 20 | "max_subgroups_hwrap": 2, 21 | "max_pargs_hwrap": 6, 22 | "max_rows_cmdline": 2, 23 | "separate_ctrl_name_with_space": false, 24 | "separate_fn_name_with_space": false, 25 | "dangle_parens": true, 26 | "dangle_align": "prefix", 27 | "min_prefix_chars": 4, 28 | "max_prefix_chars": 10, 29 | "max_lines_hwrap": 2, 30 | "line_ending": "unix", 31 | "command_case": "canonical", 32 | "keyword_case": "unchanged", 33 | "always_wrap": [], 34 | "enable_sort": true, 35 | "autosort": false, 36 | "require_valid_layout": false, 37 | "layout_passes": {} 38 | }, 39 | "markup": { 40 | "_help_bullet_char": [ 41 | "What character to use for bulleted lists" 42 | ], 43 | "bullet_char": "*", 44 | "enum_char": ".", 45 | "first_comment_is_literal": true, 46 | "literal_comment_pattern": null, 47 | "fence_pattern": "^\\s*([`~]{3}[`~]*)(.*)$", 48 | "ruler_pattern": "^\\s*[^\\w\\s]{3}.*[^\\w\\s]{3}$", 49 | "explicit_trailing_pattern": "#<", 50 | "hashruler_min_length": 10, 51 | "canonicalize_hashrulers": true, 52 | "enable_markup": true 53 | }, 54 | "lint": { 55 | "disabled_codes": [], 56 | "function_pattern": "[0-9a-z_]+", 57 | "macro_pattern": "[0-9A-Z_]+", 58 | "global_var_pattern": "[A-Z][0-9A-Z_]+", 59 | "internal_var_pattern": "_[A-Z][0-9A-Z_]+", 60 | "local_var_pattern": "[a-z][a-z0-9_]+", 61 | "private_var_pattern": "_[0-9a-z_]+", 62 | "public_var_pattern": "[A-Z][0-9A-Z_]+", 63 | "argument_var_pattern": "[a-z][a-z0-9_]+", 64 | "keyword_pattern": "[A-Z][0-9A-Z_]+", 65 | "max_conditionals_custom_parser": 2, 66 | "min_statement_spacing": 1, 67 | "max_statement_spacing": 2, 68 | "max_returns": 6, 69 | "max_branches": 12, 70 | "max_arguments": 5, 71 | "max_localvars": 15, 72 | "max_statements": 50 73 | }, 74 | "encode": { 75 | "emit_byteorder_mark": false, 76 | "input_encoding": "utf-8", 77 | "output_encoding": "utf-8" 78 | }, 79 | "misc": { 80 | "per_command": {} 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /.github/workflows/bsd.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Timo Röhling 2 | # SPDX-License-Identifier: FSFAP 3 | # 4 | # Copying and distribution of this file, with or without modification, are 5 | # permitted in any medium without royalty provided the copyright notice and 6 | # this notice are preserved. This file is offered as-is, without any warranty. 7 | # 8 | name: BSD 9 | on: 10 | push: 11 | branches: 12 | - main 13 | pull_request: 14 | branches: 15 | - main 16 | permissions: 17 | contents: read 18 | jobs: 19 | freebsd: 20 | runs-on: ubuntu-latest 21 | name: FreeBSD 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: vmactions/freebsd-vm@v1 25 | with: 26 | usesh: true 27 | prepare: | 28 | pkg install -y cmake git gmake autoconf automake 29 | run: | 30 | mkdir _build 31 | cd _build 32 | cmake .. -DDEVELOPER_BUILD=ON -DWITH_SQLITE=ON -DWITH_REDIS=ON && gmake VERBOSE=ON && ctest --output-on-failure 33 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Timo Röhling 2 | # SPDX-License-Identifier: FSFAP 3 | # 4 | # Copying and distribution of this file, with or without modification, are 5 | # permitted in any medium without royalty provided the copyright notice and 6 | # this notice are preserved. This file is offered as-is, without any warranty. 7 | # 8 | name: Continuous Integration 9 | on: 10 | push: 11 | branches: 12 | - main 13 | pull_request: 14 | branches: 15 | - main 16 | permissions: 17 | contents: read 18 | jobs: 19 | test: 20 | runs-on: ubuntu-latest 21 | strategy: 22 | matrix: 23 | milter: [MILTER=OFF, MILTER=ON] 24 | sqlite: [SQLITE=OFF, SQLITE=ON] 25 | redis: [REDIS=OFF, REDIS=ON] 26 | deps: [vendored-deps, system-deps] 27 | steps: 28 | - uses: actions/checkout@v4 29 | - name: Install dependencies 30 | run: | 31 | sudo apt-get update -qq 32 | sudo apt-get install -y cmake postfix redis ${{ matrix.deps == 'system-deps' && 'check libconfuse-dev libhiredis-dev libmilter-dev libsqlite3-dev' || '' }} 33 | - name: Build PostSRSd 34 | run: | 35 | mkdir _build 36 | cd _build 37 | cmake .. -DDEVELOPER_BUILD=ON -DWITH_${{ matrix.milter }} -DWITH_${{ matrix.sqlite }} -DWITH_${{ matrix.redis }} ${{ matrix.deps == 'system-deps' && '-DFETCHCONTENT_FULLY_DISCONNECTED=ON -DFETCHCONTENT_TRY_FIND_PACKAGE_MODE=ALWAYS' || '' }} 38 | cmake --build . --verbose 39 | - name: Run tests 40 | run: | 41 | cd _build 42 | ctest --output-on-failure 43 | - name: Install and start PostSRSd daemon 44 | run: | 45 | cd _build 46 | sudo make install 47 | sed -e 's/^#srs-domain.*/srs-domain = "example.com"/' /usr/local/share/doc/postsrsd/postsrsd.conf | sudo tee /usr/local/etc/postsrsd.conf 48 | sudo systemctl enable postsrsd 49 | sudo systemctl start postsrsd 50 | sudo journalctl --no-pager -u postsrsd 51 | sudo systemctl --no-pager status postsrsd 52 | - name: Test Postfix integration 53 | run: | 54 | postmap -q test@otherdomain.com socketmap:unix:/var/spool/postfix/srs:forward | tee /tmp/srs-alias.txt 55 | postmap -q "$(cat /tmp/srs-alias.txt)" socketmap:unix:/var/spool/postfix/srs:reverse 56 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Timo Röhling 2 | # SPDX-License-Identifier: FSFAP 3 | # 4 | # Copying and distribution of this file, with or without modification, are 5 | # permitted in any medium without royalty provided the copyright notice and 6 | # this notice are preserved. This file is offered as-is, without any warranty. 7 | # 8 | name: Create Github release 9 | on: 10 | push: 11 | tags: 12 | - "2.*" 13 | permissions: 14 | contents: write 15 | jobs: 16 | release: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Install dependencies 21 | run: | 22 | sudo apt-get update -qq 23 | sudo apt-get install -y cmake musl-dev musl-tools 24 | - name: Build PostSRSd 25 | run: | 26 | mkdir _build 27 | cd _build 28 | cmake .. -DCMAKE_BUILD_TYPE=MinSizeRel -DCMAKE_C_COMPILER=musl-gcc -DCMAKE_EXE_LINKER_FLAGS=-static -DBUILD_TESTING=OFF -DWITH_SQLITE=ON -DWITH_REDIS=ON -DGENERATE_SRS_SECRET=OFF 29 | make VERBOSE=ON 30 | - name: Install PostSRSd 31 | run: | 32 | cd _build 33 | make install DESTDIR=$PWD/_install 34 | - name: Create TAR 35 | run: tar -C_build/_install -cvzf postsrsd-x86_64-musl.tar.gz ./ 36 | - name: Create release 37 | uses: softprops/action-gh-release@v1 38 | with: 39 | files: postsrsd-x86_64-musl.tar.gz 40 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v5.0.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-yaml 10 | - id: check-added-large-files 11 | - repo: https://github.com/pre-commit/mirrors-clang-format 12 | rev: v20.1.0 13 | hooks: 14 | - id: clang-format 15 | - repo: https://github.com/psf/black 16 | rev: 25.1.0 17 | hooks: 18 | - id: black 19 | - repo: https://github.com/cheshirekow/cmake-format-precommit 20 | rev: v0.6.13 21 | hooks: 22 | - id: cmake-format 23 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | .. 2 | PostSRSd - Sender Rewriting Scheme daemon for Postfix 3 | Copyright 2012-2022 Timo Röhling 4 | SPDX-License-Identifier: GPL-3.0-only 5 | 6 | ######### 7 | Changelog 8 | ######### 9 | 10 | 2.0.11 11 | ====== 12 | 13 | Fixed 14 | ----- 15 | 16 | * Run `autoreconf` to prevent confuse build failures with newer 17 | autoconf/automake releases 18 | * Fix build failures with libcheck if libsubunit ist installed 19 | (`#161 `_) 31 | 32 | Added 33 | ----- 34 | 35 | * Support for building against system libmilter library 36 | 37 | 2.0.9 38 | ===== 39 | 40 | Fixed 41 | ----- 42 | 43 | * Fixed build with system libraries 44 | (`#176 `_) 45 | 46 | Changed 47 | ------- 48 | 49 | * Create sockets non-blocking and with close-on-exec enabled 50 | 51 | 2.0.8 52 | ===== 53 | 54 | Fixed 55 | ----- 56 | 57 | * Fixed socket creation for Milter 58 | * Fixed Milter issue with IPv6 clients 59 | (`#156 `_) 60 | 61 | Added 62 | ----- 63 | 64 | * Support for system user management with ``sysusers.d`` 65 | * Better customization of the PostSRSd build with 66 | ``POSTSRSD_CONFIGDIR`` and ``INSTALL_SYSTEMD_SERVICE`` 67 | 68 | Changed 69 | ------- 70 | 71 | * Improved documentation of the PostSRSd example configuration 72 | 73 | Contributions 74 | ------------- 75 | 76 | * Richard Hansen (`#155 `_, 77 | `#157 `_) 78 | 79 | 2.0.7 80 | ===== 81 | 82 | Fixed 83 | ----- 84 | 85 | * the parser callback for the ``original-envelope`` option used the 86 | wrong return type, which could prevent the ``database`` mode from 87 | activating 88 | * PostSRSd is confirmed to build and run on FreeBSD now 89 | 90 | 2.0.6 91 | ===== 92 | 93 | Added 94 | ----- 95 | 96 | * New configuration option ``debug`` to increase log verbosity. 97 | 98 | Changed 99 | ------- 100 | 101 | * Reduced default log verbosity: PostSRSd no longer prints 102 | messages for mail addresses which need no rewrite 103 | (`#149 `_) 104 | 105 | 2.0.5 106 | ===== 107 | 108 | Fixed 109 | ----- 110 | 111 | * Do not try to set Keep-Alive on Redis unix sockets 112 | (`#146 `_) 113 | 114 | 2.0.4 115 | ===== 116 | 117 | Fixed 118 | ----- 119 | 120 | * Worked around EXCLUDE_FROM_ALL bug in CMake 3.20.x and older 121 | * Fixed a few compiler warnings in the test suite 122 | 123 | Added 124 | ----- 125 | 126 | * Added support for musl as libc alternative 127 | * Added support for CPack to generate installable packages 128 | * Added new CLI option -h to print a summary of CLI options 129 | 130 | Changed 131 | ------- 132 | 133 | * The test suite no longer requires ``faketime`` as dependency 134 | * Improved error logging 135 | 136 | 137 | 2.0.3 138 | ===== 139 | 140 | Fixed 141 | ----- 142 | 143 | * Close socketmap connection in main process to prevent resource 144 | exhaustion (`#141 `_) 145 | * Explicitly set 0666 permissions on socketmap unix socket 146 | (`#141 `_) 147 | 148 | 2.0.2 149 | ===== 150 | 151 | Fixed 152 | ----- 153 | 154 | * Improved detection logic for systemd system unit directory 155 | (`#132 `_) 156 | * Drop supplementary groups when relinquishing root privileges 157 | (`#133 `_) 158 | 159 | 160 | 2.0.1 161 | ===== 162 | 163 | Fixed 164 | ----- 165 | 166 | * Fixed improper linking against the pthread library on systems 167 | where pthread is separate from libc 168 | (`#130 `_) 169 | 170 | 171 | 2.0.0 172 | ===== 173 | 174 | Added 175 | ----- 176 | 177 | * Added proper configuration file format 178 | * Added support for unix sockets 179 | * Added new rewrite mode with database backend 180 | * Added experimental milter support 181 | 182 | Changed 183 | ------- 184 | 185 | * PostSRSd uses ``socketmap`` tables instead of ``tcp`` tables now 186 | 187 | Removed 188 | ------- 189 | 190 | * Removed AppArmor and SELinux profiles 191 | * Removed support for all init systems except systemd 192 | (Pull requests for needed init systems are welcome) 193 | 194 | 195 | 1.12 196 | ==== 197 | 198 | Fixed 199 | ----- 200 | 201 | * Explicitly clear ``O_NONBLOCK`` to avoid inherited non-blocking sockets 202 | on some operating systems 203 | (`#117 `_) 204 | * Do not close all file descriptors up to ``_SC_MAX_OPEN``, as this limit 205 | tends to be absurdly high in Docker containers 206 | (`#122 `_) 207 | * Check for the existence of the ``faketime`` tool before using it in the 208 | unit tests. 209 | 210 | 211 | 1.11 212 | ==== 213 | 214 | Security 215 | -------- 216 | 217 | * The subprocess that talks to Postfix could be caused to hang with a very 218 | long email address 219 | (`077be98d `_) 220 | 221 | 1.10 222 | ==== 223 | 224 | Security 225 | -------- 226 | 227 | * Fixed CVE-2020-35573: PostSRSd could be tricked into consuming a lot of CPU 228 | time with an SRS address that has a very long time stamp tag 229 | (`4733fb11 `_) 230 | 231 | Fixed 232 | ----- 233 | 234 | * Fixed a bug where PostSRSd would occasionally create invalid SRS addresses 235 | if the used secret is extremely long 236 | 237 | 238 | 1.9 239 | === 240 | 241 | Hotfix release 242 | 243 | Added 244 | ----- 245 | 246 | * Added test that systemd service file is working properly 247 | 248 | Fixed 249 | ----- 250 | 251 | * Fixed systemd service file 252 | 253 | 254 | 1.8 255 | === 256 | 257 | Added 258 | ----- 259 | 260 | * Added "Always Rewrite" option 261 | (`#97 `_) 262 | * Added blackbox testing for PostSRSd daemon 263 | 264 | Changed 265 | ------- 266 | 267 | * Improved syslog messages 268 | 269 | Fixed 270 | ----- 271 | 272 | * Fixed AppArmor and SELinux profiles 273 | 274 | 275 | 1.7 276 | === 277 | 278 | Changed 279 | ------- 280 | 281 | * Improved systemd auto detection 282 | * Drop group privileges as well as user privileges 283 | * Merged Debian adaptations (Thanks to Oxan van Leeuwen) 284 | 285 | Removed 286 | ------- 287 | 288 | * CMake 2.x support 289 | 290 | 291 | 1.6 292 | === 293 | 294 | Added 295 | ----- 296 | 297 | * Somewhat usable unit tests 298 | 299 | Fixed 300 | ----- 301 | 302 | * Fixed Big Endian issue with SHA-1 implementation 303 | (`#90 `_) 304 | 305 | 1.5 306 | === 307 | 308 | Added 309 | ----- 310 | 311 | * Add configuration options for listening network interface 312 | 313 | Changed 314 | ------- 315 | 316 | * Close all open file descriptors on startup 317 | 318 | Fixed 319 | ----- 320 | 321 | * Fixed SELinux policy 322 | * Fixed handling of excluded domains in systemd startup file 323 | 324 | 325 | 1.4 326 | === 327 | 328 | Added 329 | ----- 330 | 331 | * Added dual stack support 332 | 333 | Fixed 334 | ----- 335 | 336 | * Make startup scripts more robust in case of configuration errors 337 | * Improved BSD compatibility 338 | 339 | 340 | 1.3 341 | === 342 | 343 | Added 344 | ----- 345 | 346 | * Make SRS separator configurable 347 | * Added support for even more init systems 348 | 349 | 350 | 1.2 351 | === 352 | 353 | Added 354 | ----- 355 | 356 | * Added support for more init systems 357 | 358 | Changed 359 | ------- 360 | 361 | * Listen to 127.0.0.1 by default 362 | 363 | Fixed 364 | ----- 365 | 366 | * Load correct timezone for logging 367 | 368 | 369 | 1.1 370 | === 371 | 372 | Fixed 373 | ----- 374 | 375 | * Fixed various issues with the CMake script 376 | * Fixed command line parsing bug 377 | 378 | 379 | 1.0 380 | === 381 | * First stable release 382 | -------------------------------------------------------------------------------- /LICENSES/BSD-3-Clause.txt: -------------------------------------------------------------------------------- 1 | Redistribution and use in source and binary forms, with or without 2 | modification, are permitted provided that the following conditions are met: 3 | 4 | * Redistributions of source code must retain the above copyright notice, 5 | this list of conditions and the following disclaimer. 6 | * Redistributions in binary form must reproduce the above copyright 7 | notice, this list of conditions and the following disclaimer in the 8 | documentation and/or other materials provided with the distribution. 9 | * Neither the name of the project nor the names of its 10 | contributors may be used to endorse or promote products derived from 11 | this software without specific prior written permission. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 20 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /LICENSES/FSFAP.txt: -------------------------------------------------------------------------------- 1 | Copying and distribution of this file, with or without modification, are 2 | permitted in any medium without royalty provided the copyright notice and 3 | this notice are preserved. This file is offered as-is, without any warranty. 4 | -------------------------------------------------------------------------------- /LICENSES/FSFUL.txt: -------------------------------------------------------------------------------- 1 | The copyright holder gives unlimited permission to copy, distribute and modify 2 | this file. 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. 2 | PostSRSd - Sender Rewriting Scheme daemon for Postfix 3 | Copyright 2012-2023 Timo Röhling 4 | SPDX-License-Identifier: GPL-3.0-only 5 | 6 | ======== 7 | PostSRSd 8 | ======== 9 | 10 | Sender Rewriting Scheme daemon for Postfix 11 | 12 | 13 | Overview 14 | -------- 15 | 16 | The Sender Rewriting Scheme (SRS) is a technique to forward mails from domains 17 | which deploy the Sender Policy Framework (SPF) to prohibit other Mail Transfer 18 | Agents (MTAs) from sending mails on their behalf. With SRS, an MTA can 19 | circumvent SPF restrictions by replacing the envelope sender with a temporary 20 | email address from one of their own domains. This temporary address is bound to 21 | the original sender and only valid for a certain amount of time, which prevents 22 | abuse by spammers. 23 | 24 | 25 | Installation 26 | ------------ 27 | 28 | Prebuilt packages 29 | ~~~~~~~~~~~~~~~~~ 30 | 31 | If your Linux distribution has a sufficiently recent PostSRSd package, install 32 | it! Unless you need a specific new feature or bugfix from a newer version, it 33 | will be much less of a maintenance burden. 34 | 35 | If you are interested in packaging PostSRSd for a Linux distribution, have a 36 | look at the packaging_ notes. In particular, we are currently looking for a new 37 | Debian maintainer (`#145 `_). 38 | 39 | .. _packaging: doc/packaging.rst 40 | 41 | Building from source 42 | ~~~~~~~~~~~~~~~~~~~~ 43 | 44 | Fetch the latest source tarball or clone the repository from Github_, unpack it 45 | and run:: 46 | 47 | cd path/to/source 48 | mkdir _build && cd _build 49 | cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local 50 | make -j 51 | sudo make install 52 | 53 | .. _Github: https://github.com/roehling/postsrsd/releases/latest 54 | 55 | PostSRSd has a few external build dependencies: 56 | 57 | - CMake_ version 3.14 or newer 58 | - gcc_ or a similar C99 capable C compiler. 59 | - pkgconf_ or pkg-config is optional to improve detection of system settings 60 | - libConfuse_ is required to parse the configuration file. 61 | - sqlite3_ is optional to store envelope senders; 62 | enable it with ``-DWITH_SQLITE=ON`` as additional argument for ``cmake``. 63 | - hiredis_ is an optional alternative to store envelope senders in Redis; 64 | enable it with ``-DWITH_REDIS=ON``. 65 | - libMilter_ is needed only if you wish to configure PostSRSd as milter; 66 | enable it with ``-DWITH_MILTER=ON``. 67 | - check_ is needed if you want to build and run the unit test suite; 68 | otherwise disable it with ``-DBUILD_TESTING=OFF``. 69 | - Python_ is needed for the optional blackbox tests. 70 | 71 | PostSRSd relies on the FetchContent_ module of CMake for its dependency 72 | resolution. Please refer to its documentation if you wish to tweak the 73 | discovery process. 74 | 75 | .. _CMake: https://cmake.org 76 | .. _gcc: https://gcc.gnu.org 77 | .. _pkgconf: http://pkgconf.org 78 | .. _libConfuse: https://github.com/libconfuse/libconfuse 79 | .. _sqlite3: https://sqlite.org 80 | .. _hiredis: https://github.com/redis/hiredis 81 | .. _libMilter: https://github.com/jons/libmilter 82 | .. _check: https://github.com/libcheck/check 83 | .. _FetchContent: https://cmake.org/cmake/help/latest/module/FetchContent.html 84 | .. _Python: https://www.python.org 85 | 86 | Configuration 87 | ------------- 88 | 89 | PostSRSd itself is configured by ``postsrsd.conf`` (see the example_ for a 90 | detailed documentation of all options). PostSRSd will look for this file in 91 | ``/usr/local/etc``. The most important configuration options are ``domains`` 92 | (or ``domains-file``), so PostSRSd knows about your local domains, and 93 | ``secrets-file`` with a secret passphrase for authentication. The other options 94 | often work out of the box. You can also find the example configuration 95 | installed in ``/usr/local/share/postsrsd``. Feel free to use it as base for 96 | your own configuration. 97 | 98 | Postfix Setup 99 | ~~~~~~~~~~~~~ 100 | 101 | For integration with Postfix, the recommended mechanism is via the 102 | ``canonical`` maps of the ``cleanup`` daemon. Add the following snippet to your 103 | ``/etc/postfix/main.cf``:: 104 | 105 | sender_canonical_maps = socketmap:unix:srs:forward 106 | sender_canonical_classes = envelope_sender 107 | recipient_canonical_maps = socketmap:unix:srs:reverse 108 | recipient_canonical_classes = envelope_recipient, header_recipient 109 | 110 | The ``srs`` part in the lookup table mappings above is the path to the unix 111 | socket relative to ``/var/spool/postfix``; you will have to change this if you 112 | change the ``socketmap`` configuration of PostSRSd. If you prefer a TCP 113 | connection, e.g. ``inet:localhost:10003``, you need to change the mapping to 114 | something like ``socketmap:inet:localhost:10003:forward``. 115 | 116 | .. _example: doc/postsrsd.conf 117 | 118 | Experimental Milter Support 119 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 120 | 121 | PostSRSd 2.x has added optional support for the Milter protocol. If you enabled 122 | it at compile time, you can set the ``milter`` option in ``postsrsd.conf`` and 123 | add the corresponding line to your ``etc/postfix/main.cf``:: 124 | 125 | smtpd_milters = unix:srs_milter 126 | 127 | Note that the Milter code is less tested and should be considered experimental 128 | for now and not ready for production. Feel free to report bugs or open pull 129 | requests if you try it out, though. 130 | 131 | Migrating from version 1.x 132 | -------------------------- 133 | 134 | Most configuration options can no longer be configured with command line 135 | arguments, so you will have to set them in ``postsrsd.conf``. PostSRSd 1.x used 136 | shell variables in ``/etc/default/postsrsd``. If you migrate your settings, you 137 | should set 138 | 139 | - ``srs-domain`` to the value from ``SRS_DOMAIN`` 140 | - ``domains`` to the list of values from ``SRS_EXCLUDE_DOMAINS`` 141 | - ``secrets-file`` to the file name from ``SRS_SECRET`` 142 | - ``unprivileged-user`` to the user name from ``RUN_AS`` 143 | - ``chroot-dir`` to the directory from ``CHROOT`` 144 | 145 | Be aware that PostSRSd 2.x uses ``socketmap:`` tables, which are NOT compatible 146 | with ``tcp:`` tables. This also means that PostSRSd 2.x requires at least 147 | Postfix 2.10 now, and you need to update your Postfix configuration as detailed 148 | above. 149 | 150 | Frequently Asked Questions 151 | -------------------------- 152 | 153 | * **Can I configure PostSRSd so it will only rewrite the envelope sender if the 154 | email is not delivered locally?** 155 | 156 | This is not supported currently but might be added to the milter at some 157 | point in the future. 158 | 159 | If PostSRSd is integrated with Postfix using the ``canonical`` maps, it is 160 | almost impossible, because the canonicalization occurs before any routing 161 | decision is made. Only if you happen to use separate Postfix server instances 162 | for forwarding and local delivery, you can trivially configure PostSRSd this 163 | way. 164 | 165 | * **I am serving multiple domains with my MTA. Can I configure PostSRSd to 166 | rewrite addresses to the specific domain for which an email is forwarded?** 167 | 168 | If PostSRSd is integrated with Postfix using the ``canonical`` maps, this is 169 | not possible, because PostSRSd processes sender and recipient addresses 170 | separately and never sees the email context. 171 | 172 | If PostSRSd is configured as milter, it might be theoretically possible, but 173 | it is not supported yet, for two reasons: 174 | 175 | 1. It is not trivial to implement and conflicts with other interesting 176 | features such as rewriting only if the email is actually forwarded. 177 | 2. The SRS address is normally not visible to the recipient anyway. 178 | 179 | It is much simpler and more robust to have a dedicated SRS (sub-)domain. You 180 | need to pick a domain for the reverse DNS lookup of your MTA IP address 181 | anyway, so setup an ``srs`` subdomain there and use it for SRS rewriting. 182 | 183 | * **I configured PostSRSd correctly; why are some of my emails still rejected 184 | with a DMARC failure?** 185 | 186 | Short Answer: Because the originating MTA is misconfigured. 187 | 188 | Long Answer: DMARC has two conditions for an email, but either of them is 189 | sufficient to pass the DMARC check: 190 | 191 | 1. The SMTP envelope sender must have the same domain as the 192 | ``From:`` address in the mail header. 193 | 2. The email must have a valid DKIM signature from the domain of the 194 | ``From:`` address. 195 | 196 | The first condition in combination with SPF prevents mail forwarding by 197 | unauthorized third parties, the second condition in combination with DKIM 198 | prevents sender address spoofing. Effectively, DMARC only allows mail 199 | forwarding if the mail is not tampered with. 200 | 201 | By design, SRS must break the first condition, but it will preserve the 202 | second, if the originating MTA signs all outgoing mails with DKIM. 203 | 204 | Unfortunately, some mail admins forget (or misconfigure) DKIM, which 205 | effectively breaks forwarding for *everyone*. Try to contact the mail 206 | administrator for the sending domain and tell them to fix their setup. 207 | -------------------------------------------------------------------------------- /REUSE.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | SPDX-PackageName = "PostSRSd" 3 | SPDX-PackageSupplier = "Timo Röhling " 4 | SPDX-PackageDownloadLocation = "https://github.com/roehling/postsrsd" 5 | 6 | [[annotations]] 7 | path = "**" 8 | precedence = "aggregate" 9 | SPDX-FileCopyrightText = "2012-2023, Timo Röhling " 10 | SPDX-License-Identifier = "GPL-3.0-only" 11 | 12 | [[annotations]] 13 | path = [".clang-format", ".cmake-format.json", ".pre-commit-config.yaml", "data/**", "doc/postsrsd.conf"] 14 | precedence = "aggregate" 15 | SPDX-FileCopyrightText = "2012-2023, Timo Röhling " 16 | SPDX-License-Identifier = "FSFUL" 17 | 18 | [[annotations]] 19 | path = [".github/**", "cmake/**"] 20 | precedence = "aggregate" 21 | SPDX-FileCopyrightText = "2022-2023, Timo Röhling " 22 | SPDX-License-Identifier = "FSFAP" 23 | 24 | [[annotations]] 25 | path = ["src/sha1.c", "src/sha1.h", "src/srs2.c", "src/srs2.h"] 26 | precedence = "aggregate" 27 | SPDX-FileCopyrightText = ["Gisle Ass, Peter C. Gutmann, Bruce Schneier", "2004, Shevek ", "2012-2023, Timo Röhling "] 28 | SPDX-License-Identifier = "BSD-3-Clause" 29 | -------------------------------------------------------------------------------- /cmake/FindCheck.cmake: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Timo Röhling 2 | # SPDX-License-Identifier: FSFAP 3 | # 4 | # Copying and distribution of this file, with or without modification, are 5 | # permitted in any medium without royalty provided the copyright notice and 6 | # this notice are preserved. This file is offered as-is, without any warranty. 7 | # 8 | include(FindPackageHandleStandardArgs) 9 | find_package(Check CONFIG QUIET) 10 | if(TARGET Check::check) 11 | find_package_handle_standard_args(Check CONFIG_MODE) 12 | else() 13 | find_package(PkgConfig QUIET) 14 | if(PkgConfig_FOUND) 15 | pkg_search_module(PC_CHECK QUIET check) 16 | pkg_search_module(PC_SUBUNIT QUIET libsubunit) 17 | endif() 18 | find_path(Check_INCLUDE_DIR check.h HINTS ${PC_CHECK_INCLUDE_DIRS}) 19 | find_library( 20 | Check_LIBRARY 21 | NAMES check_pic check 22 | HINTS ${PC_CHECK_LIBRARY_DIRS} 23 | ) 24 | find_library(Check_subunit_LIBRARY subunit HINTS ${PC_SUBUNIT_LIBRARY_DIRS}) 25 | find_library(Check_m_LIBRARY m) 26 | find_library(Check_rt_LIBRARY rt) 27 | find_package(Threads REQUIRED) 28 | find_package_handle_standard_args( 29 | Check 30 | FOUND_VAR Check_FOUND 31 | REQUIRED_VARS Check_INCLUDE_DIR Check_LIBRARY 32 | ) 33 | if(Check_FOUND AND NOT TARGET Check::check) 34 | set(Check_DEPS "Threads::Threads") 35 | if(Check_subunit_LIBRARY) 36 | list(APPEND Check_DEPS "${Check_subunit_LIBRARY}") 37 | endif() 38 | if(Check_m_LIBRARY) 39 | list(APPEND Check_DEPS "${Check_m_LIBRARY}") 40 | endif() 41 | if(Check_rt_LIBRARY) 42 | list(APPEND Check_DEPS "${Check_rt_LIBRARY}") 43 | endif() 44 | add_library(Check::check UNKNOWN IMPORTED) 45 | set_target_properties( 46 | Check::check 47 | PROPERTIES IMPORTED_LOCATION "${Check_LIBRARY}" 48 | INTERFACE_INCLUDE_DIRECTORIES "${Check_INCLUDE_DIR}" 49 | INTERFACE_LINK_LIBRARIES "${Check_DEPS}" 50 | ) 51 | endif() 52 | endif() 53 | -------------------------------------------------------------------------------- /cmake/FindConfuse.cmake: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Timo Röhling 2 | # SPDX-License-Identifier: FSFAP 3 | # 4 | # Copying and distribution of this file, with or without modification, are 5 | # permitted in any medium without royalty provided the copyright notice and 6 | # this notice are preserved. This file is offered as-is, without any warranty. 7 | # 8 | include(FindPackageHandleStandardArgs) 9 | find_package(PkgConfig QUIET) 10 | if(PkgConfig_FOUND) 11 | pkg_search_module(PC_CONFUSE QUIET libconfuse) 12 | endif() 13 | find_path(Confuse_INCLUDE_DIR confuse.h HINTS ${PC_CONFUSE_INCLUDE_DIRS}) 14 | find_library(Confuse_LIBRARY confuse HINTS ${PC_CONFUSE_LIBRARY_DIRS}) 15 | find_package_handle_standard_args( 16 | Confuse 17 | FOUND_VAR Confuse_FOUND 18 | REQUIRED_VARS Confuse_INCLUDE_DIR Confuse_LIBRARY 19 | ) 20 | if(Confuse_FOUND AND NOT TARGET Confuse::Confuse) 21 | add_library(Confuse::Confuse UNKNOWN IMPORTED) 22 | set_target_properties( 23 | Confuse::Confuse 24 | PROPERTIES IMPORTED_LOCATION "${Confuse_LIBRARY}" 25 | INTERFACE_INCLUDE_DIRECTORIES "${Confuse_INCLUDE_DIR}" 26 | ) 27 | endif() 28 | -------------------------------------------------------------------------------- /cmake/FindHiredis.cmake: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Timo Röhling 2 | # SPDX-License-Identifier: FSFAP 3 | # 4 | # Copying and distribution of this file, with or without modification, are 5 | # permitted in any medium without royalty provided the copyright notice and 6 | # this notice are preserved. This file is offered as-is, without any warranty. 7 | # 8 | include(FindPackageHandleStandardArgs) 9 | find_package(PkgConfig QUIET) 10 | if(PkgConfig_FOUND) 11 | pkg_search_module(PC_HIREDIS QUIET hiredis) 12 | endif() 13 | find_path( 14 | Hiredis_INCLUDE_DIR hiredis.h 15 | PATH_SUFFIXES hiredis 16 | HINTS ${PC_HIREDIS_INCLUDE_DIRS} 17 | ) 18 | find_library(Hiredis_LIBRARY hiredis HINTS ${PC_HIREDIS_LIBRARY_DIRS}) 19 | find_package_handle_standard_args( 20 | Hiredis 21 | FOUND_VAR Hiredis_FOUND 22 | REQUIRED_VARS Hiredis_INCLUDE_DIR Hiredis_LIBRARY 23 | ) 24 | if(Hiredis_FOUND AND NOT TARGET hiredis::hiredis) 25 | add_library(hiredis::hiredis UNKNOWN IMPORTED) 26 | set_target_properties( 27 | hiredis::hiredis 28 | PROPERTIES IMPORTED_LOCATION "${Hiredis_LIBRARY}" 29 | INTERFACE_INCLUDE_DIRECTORIES "${Hiredis_INCLUDE_DIR}" 30 | ) 31 | endif() 32 | -------------------------------------------------------------------------------- /cmake/FindLibMilter.cmake: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Timo Röhling 2 | # SPDX-License-Identifier: FSFAP 3 | # 4 | # Copying and distribution of this file, with or without modification, are 5 | # permitted in any medium without royalty provided the copyright notice and 6 | # this notice are preserved. This file is offered as-is, without any warranty. 7 | # 8 | include(FindPackageHandleStandardArgs) 9 | find_package(PkgConfig QUIET) 10 | if(PkgConfig_FOUND) 11 | pkg_search_module(PC_MILTER QUIET milter) 12 | endif() 13 | find_path( 14 | LibMilter_INCLUDE_DIR mfapi.h 15 | PATH_SUFFIXES libmilter 16 | HINTS ${PC_MILTER_INCLUDE_DIRS} 17 | ) 18 | find_library(LibMilter_LIBRARY milter HINTS ${PC_MILTER_LIBRARY_DIRS}) 19 | find_package_handle_standard_args( 20 | LibMilter 21 | FOUND_VAR LibMilter_FOUND 22 | REQUIRED_VARS LibMilter_INCLUDE_DIR LibMilter_LIBRARY 23 | ) 24 | if(LibMilter_FOUND AND NOT TARGET LibMilter::LibMilter) 25 | add_library(LibMilter::LibMilter UNKNOWN IMPORTED) 26 | set_target_properties( 27 | LibMilter::LibMilter 28 | PROPERTIES IMPORTED_LOCATION "${LibMilter_LIBRARY}" 29 | INTERFACE_INCLUDE_DIRECTORIES "${LibMilter_INCLUDE_DIR}" 30 | ) 31 | endif() 32 | -------------------------------------------------------------------------------- /cmake/Findsqlite3.cmake: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Timo Röhling 2 | # SPDX-License-Identifier: FSFAP 3 | # 4 | # Copying and distribution of this file, with or without modification, are 5 | # permitted in any medium without royalty provided the copyright notice and 6 | # this notice are preserved. This file is offered as-is, without any warranty. 7 | # 8 | include(FindPackageHandleStandardArgs) 9 | find_package(PkgConfig QUIET) 10 | if(PkgConfig_FOUND) 11 | pkg_search_module(PC_SQLITE3 QUIET sqlite3) 12 | endif() 13 | find_path(sqlite3_INCLUDE_DIR sqlite3.h HINTS ${PC_SQLITE3_INCLUDE_DIRS}) 14 | find_library(sqlite3_LIBRARY sqlite3 HINTS ${PC_SQLITE3_LIBRARY_DIRS}) 15 | find_package_handle_standard_args( 16 | sqlite3 17 | FOUND_VAR sqlite3_FOUND 18 | REQUIRED_VARS sqlite3_INCLUDE_DIR sqlite3_LIBRARY 19 | ) 20 | if(sqlite3_FOUND AND NOT TARGET sqlite3::sqlite3) 21 | add_library(sqlite3::sqlite3 UNKNOWN IMPORTED) 22 | set_target_properties( 23 | sqlite3::sqlite3 24 | PROPERTIES IMPORTED_LOCATION "${sqlite3_LIBRARY}" 25 | INTERFACE_INCLUDE_DIRECTORIES "${sqlite3_INCLUDE_DIR}" 26 | ) 27 | endif() 28 | -------------------------------------------------------------------------------- /cmake/utils.cmake: -------------------------------------------------------------------------------- 1 | # Copyright 2022-2023 Timo Röhling 2 | # SPDX-License-Identifier: FSFAP 3 | # 4 | # Copying and distribution of this file, with or without modification, are 5 | # permitted in any medium without royalty provided the copyright notice and 6 | # this notice are preserved. This file is offered as-is, without any warranty. 7 | # 8 | include(CMakeParseArguments) 9 | include(ExternalProject) 10 | include(FetchContent) 11 | 12 | function(add_autotools_dependency name) 13 | cmake_parse_arguments(arg "" "LIBRARY_NAME;EXPORTED_TARGET" "" ${ARGN}) 14 | FetchContent_MakeAvailable(${name}) 15 | if(NOT TARGET ${arg_EXPORTED_TARGET}) 16 | find_program(MAKE_EXECUTABLE NAMES gmake make mingw32-make REQUIRED) 17 | set(library_file 18 | "${CMAKE_STATIC_LIBRARY_PREFIX}${arg_LIBRARY_NAME}${CMAKE_STATIC_LIBRARY_SUFFIX}" 19 | ) 20 | string(TOLOWER "${name}" lc_name) 21 | string(TOUPPER "${CMAKE_BUILD_TYPE}" uc_build_type) 22 | if(CMAKE_C_COMPILER_AR) 23 | set(ar_executable "${CMAKE_C_COMPILER_AR}") 24 | else() 25 | set(ar_executable "${CMAKE_AR}") 26 | endif() 27 | if(CMAKE_C_COMPILER_LAUNCHER) 28 | set(cc_executable 29 | "${CMAKE_C_COMPILER_LAUNCHER} ${CMAKE_C_COMPILER}" 30 | ) 31 | else() 32 | set(cc_executable "${CMAKE_C_COMPILER}") 33 | endif() 34 | ExternalProject_Add( 35 | Ext${name} 36 | SOURCE_DIR "${${lc_name}_SOURCE_DIR}" 37 | UPDATE_DISCONNECTED TRUE 38 | PATCH_COMMAND 39 | command -v autoreconf && autoreconf 40 | CONFIGURE_COMMAND 41 | /configure --disable-shared --prefix= 42 | "CC=${cc_executable}" "AR=${ar_executable}" 43 | "RANLIB=${CMAKE_RANLIB}" "MAKE=${MAKE_EXECUTABLE}" 44 | "CFLAGS=${CMAKE_C_FLAGS} ${CMAKE_C_FLAGS_${uc_build_type}}" 45 | BUILD_COMMAND ${MAKE_EXECUTABLE} -j 46 | INSTALL_COMMAND ${MAKE_EXECUTABLE} -j install 47 | TEST_COMMAND "" 48 | BUILD_BYPRODUCTS /lib/${library_file} 49 | ) 50 | ExternalProject_Get_Property(Ext${name} INSTALL_DIR) 51 | add_library(${arg_EXPORTED_TARGET} STATIC IMPORTED) 52 | set_target_properties( 53 | ${arg_EXPORTED_TARGET} 54 | PROPERTIES IMPORTED_LOCATION "${INSTALL_DIR}/lib/${library_file}" 55 | INTERFACE_INCLUDE_DIRECTORIES "${INSTALL_DIR}/include" 56 | ) 57 | add_dependencies(${arg_EXPORTED_TARGET} Ext${name}) 58 | file(MAKE_DIRECTORY "${INSTALL_DIR}/include") 59 | endif() 60 | endfunction() 61 | 62 | function(find_systemd_unit_destination var) 63 | if(CMAKE_INSTALL_PREFIX MATCHES "^/usr/?$") 64 | find_package(PkgConfig QUIET) 65 | if(PkgConfig_FOUND) 66 | pkg_get_variable(unitdir systemd systemdsystemunitdir) 67 | set(${var} 68 | "${unitdir}" 69 | PARENT_SCOPE 70 | ) 71 | else() 72 | set(${var} 73 | "/usr/lib/systemd/system" 74 | PARENT_SCOPE 75 | ) 76 | endif() 77 | else() 78 | set(${var} 79 | "/etc/systemd/system" 80 | PARENT_SCOPE 81 | ) 82 | endif() 83 | endfunction() 84 | 85 | function(find_systemd_sysusers_destination var) 86 | if(CMAKE_INSTALL_PREFIX MATCHES "^/usr/?$") 87 | find_package(PkgConfig QUIET) 88 | if(PkgConfig_FOUND) 89 | pkg_get_variable(sysusersdir systemd sysusersdir) 90 | set(${var} 91 | "${sysusersdir}" 92 | PARENT_SCOPE 93 | ) 94 | else() 95 | set(${var} 96 | "/usr/lib/sysusers.d" 97 | PARENT_SCOPE 98 | ) 99 | endif() 100 | else() 101 | set(${var} 102 | "/etc/sysusers.d" 103 | PARENT_SCOPE 104 | ) 105 | endif() 106 | endfunction() 107 | -------------------------------------------------------------------------------- /data/postsrsd.conf.in: -------------------------------------------------------------------------------- 1 | # PostSRSd example configuration file 2 | # Copyright 2022-2023 Timo Röhling 3 | # SPDX-License-Identifier: FSFUL 4 | # 5 | # The copyright holder gives unlimited permission to copy, distribute and modify 6 | # this file. 7 | 8 | # Local domains 9 | # Your local domains need not be rewritten, so PostSRSd has to know about them. 10 | # 11 | # Example: 12 | # domains = { "example.com", "example.org", "example.net" } 13 | # 14 | domains = {} 15 | 16 | # Local domains (file storage) 17 | # Instead of listing your local domains directly, you can also write them to a 18 | # file and have PostSRSd read it. This is particularly useful if you have a 19 | # large number of domains for which you need to act as mail forwarder. PostSRSd 20 | # reads this file before it chroots and drops root privileges. The file format 21 | # is one domain per line. 22 | # 23 | # Example: 24 | # domains-file = "@POSTSRSD_CONFIGDIR@/@PROJECT_NAME@.domains" 25 | # 26 | #domains-file = 27 | 28 | # Dedicated SRS rewrite domain. 29 | # The local domain which is used to create the ephemeral SRS envelope 30 | # addresses. It is recommended that you use a dedicated mail domain for SRS if 31 | # you serve multiple unrelated domains (e.g. for your customers), to prevent 32 | # privacy issues. If unset, the first configured local domain is used. 33 | # 34 | # Example: 35 | # srs-domain = "srs.example.com" 36 | # 37 | #srs-domain = 38 | 39 | # Socketmap lookup table for Postfix integration. 40 | # Traditionally, PostSRSd interacts with Postfix through the canonicalization 41 | # lookup tables of the cleanup daemon. If you use a unix socket, be aware that 42 | # most Postfix instances will jail their cleanup daemon in a /var/spool/postfix 43 | # chroot, so no other path will be visible to them. Unix sockets are created 44 | # before PostSRSd chroots and drops root privileges. 45 | # 46 | # Examples: 47 | # socketmap = unix:/var/spool/postfix/srs 48 | # socketmap = inet:localhost:10003 49 | # 50 | socketmap = unix:/var/spool/postfix/srs 51 | 52 | # Socketmap connection keep-alive timeout. 53 | # After PostSRSd has served a socketmap request, it will keep the connection 54 | # open for a while longer, in case Postfix has additional queries. PostSRSd 55 | # will close the connection after the configured time (in seconds) has expired. 56 | # 57 | # Examples: 58 | # keep-alive = 30 59 | # 60 | keep-alive = 30 61 | 62 | # Milter endpoint for MTA integration. 63 | # PostSRSd can act as a milter to rewrite envelope addresses if it has been 64 | # built with milter support. Unix sockets are created before PostSRSd chroots 65 | # and drops root privileges. 66 | # 67 | # Examples: 68 | # milter = unix:/var/spool/postfix/srs_milter 69 | # milter = inet:localhost:9997 70 | # 71 | #milter = 72 | 73 | # Original envelope sender handling. 74 | # When the envelope sender is rewritten, the original address can either be 75 | # embedded in the rewritten address, or stored in a local database. Embedding 76 | # makes PostSRSd work fully stateless, but the full sender address needs to fit 77 | # into the localpart of the embedded address, effectively limiting the length 78 | # of forwardable sender addresses to 51 octets. Storing the sender address in a 79 | # database circumvents this problem, but makes PostSRSd vulnerable to an 80 | # attacker sending vast amounts of emails with fake sender addresses, all of 81 | # which need to be stored in the database. 82 | # 83 | # If you are unsure which option suits your use-case best, the vast majority of 84 | # mail addresses will be relatively short, so you should pick "embedded". 85 | # 86 | # Examples: 87 | # original-envelope = embedded 88 | # original-envelope = database 89 | # 90 | original-envelope = embedded 91 | 92 | # Database for envelope sender storage. 93 | # If you decide to store envelope senders in a database, this database will be 94 | # used. The option is ignored if original-envelope is set to "embedded". Also 95 | # note that PostSRSd needs to be built with SQLite or Redis support for this. 96 | # 97 | # PostSRSd reads this database after it chroots and drops root privileges, so 98 | # the actual filename is the chroot directory joined with this filename. 99 | # 100 | # Examples: 101 | # envelope-database = "sqlite:@CHROOTABLE_DATADIR@/senders.db" 102 | # envelope-database = "redis:localhost:6379" 103 | # 104 | #envelope-database = "sqlite:@CHROOTABLE_DATADIR@/senders.db" 105 | 106 | # Secret keys for signing and verifying SRS addresses. 107 | # Rewritten addresses are tagged with a truncated HMAC-SHA1 signature, to 108 | # prevent tampering and forged envelope addresses. You can have more than 109 | # one signing secret; each line of the secrets file is considered one secret 110 | # key. If an incoming signature matches any key, it is accepted. Outgoing 111 | # signatures will always be generated with the first configured secret. 112 | # 113 | # For security reasons, you should also make sure that the file is owned and 114 | # only accessible by root (chmod 600). PostSRSd reads this file before it 115 | # chroots and drops root privileges. 116 | # 117 | # Example: 118 | # secrets-file = "@POSTSRSD_CONFIGDIR@/@PROJECT_NAME@.secret" 119 | # 120 | secrets-file = "@POSTSRSD_CONFIGDIR@/@PROJECT_NAME@.secret" 121 | 122 | # SRS tag separator 123 | # This is the character following the initial SRS0 or SRS1 tag of a generated 124 | # sender address. Valid separators are "=", "+", and "-". Unless you have a 125 | # very good reason, you should leave this setting at its default. 126 | # 127 | separator = "=" 128 | 129 | # SRS hash signature length 130 | # Any SRS address will be signed with a truncated hash to prevent tampering and 131 | # ensure that only legitimate email bounces will be returned to sender. The 132 | # default length provides adequate security without taking up too much valuable 133 | # space. Unless you know what you are doing, you should leave this setting at 134 | # its default. 135 | # 136 | # WARNING: You can break your mail server (or worse, turn it into a spam relay) 137 | # if you mess up this setting. 138 | # 139 | hash-length = 4 140 | 141 | # SRS minimum acceptable hash signature length 142 | # This is the mininum signature length that PostSRSd considers valid. It is a 143 | # separate setting because if you decide to increase the hash length, you may 144 | # want to keep accepting the shorter hashes for a 24 hour grace period. Again, 145 | # Unless you know what you are doing, you should leave this setting at its 146 | # default. 147 | # 148 | # WARNING: You can break your mail server (or worse, turn it into a spam relay) 149 | # if you mess up this setting. 150 | # 151 | hash-minimum = 4 152 | 153 | # Always rewrite sender addresses 154 | # You can force PostSRSd to rewrite any sender address, even if it has been 155 | # rewritten already. You probably do not want to do this, though. 156 | # 157 | always-rewrite = off 158 | 159 | # Execute PostSRSd as unprivileged user 160 | # Drop root privileges and run as this user before entering the main loop and 161 | # handling untrusted input. To prevent PostSRSd from changing users, set this to 162 | # the empty string. 163 | # 164 | # Example: 165 | # unprivileged-user = "nobody" 166 | # 167 | unprivileged-user = "@POSTSRSD_USER@" 168 | 169 | # Execute PostSRSd in chroot jail 170 | # PostSRSd will jail itself in the given directory, which adds an additional 171 | # layer of protection against the exploitation of security bugs in PostSRSd. To 172 | # prevent PostSRSd from chrooting, set this to the empty string. 173 | # 174 | # Example: 175 | # chroot-dir = "@CMAKE_INSTALL_FULL_LOCALSTATEDIR@/lib/@PROJECT_NAME@" 176 | # 177 | chroot-dir = "@POSTSRSD_CHROOTDIR@" 178 | 179 | # Syslog 180 | # PostSRSd writes log messages to stderr. If you enable this option, PostSRSd 181 | # will also send all messages to the syslog mail facility. 182 | # 183 | syslog = off 184 | 185 | # Debug 186 | # This option makes PostSRSd more verbose in its logging, which can be useful 187 | # to hunt down configuration problems. 188 | # 189 | debug = off 190 | -------------------------------------------------------------------------------- /data/postsrsd.service.in: -------------------------------------------------------------------------------- 1 | # PostSRSd systemd service file 2 | # Copyright 2022-2023 Timo Röhling 3 | # SPDX-License-Identifier: FSFUL 4 | # 5 | # The copyright holder gives unlimited permission to copy, distribute and modify 6 | # this file. 7 | 8 | [Unit] 9 | Description=Sender Rewriting Scheme daemon for Postfix 10 | Before=postfix.service 11 | After=network.target 12 | 13 | [Service] 14 | ExecStart=@CMAKE_INSTALL_FULL_SBINDIR@/postsrsd -C @POSTSRSD_CONFIGDIR@/@PROJECT_NAME@.conf 15 | 16 | [Install] 17 | WantedBy=multi-user.target 18 | -------------------------------------------------------------------------------- /data/sysusers.conf.in: -------------------------------------------------------------------------------- 1 | # PostSRSd sysusers.d configuration file 2 | # Copyright 2023 Timo Röhling 3 | # SPDX-License-Identifier: FSFUL 4 | # 5 | # The copyright holder gives unlimited permission to copy, distribute and modify 6 | # this file. 7 | u @POSTSRSD_USER@ - "PostSRSd user" - - 8 | -------------------------------------------------------------------------------- /doc/packaging.rst: -------------------------------------------------------------------------------- 1 | .. 2 | PostSRSd - Sender Rewriting Scheme daemon for Postfix 3 | Copyright 2012-2023 Timo Röhling 4 | SPDX-License-Identifier: GPL-3.0-only 5 | 6 | ======================== 7 | PostSRSd Packaging Notes 8 | ======================== 9 | 10 | Introduction 11 | ------------ 12 | 13 | Thank you for taking an interest in PostSRSd and for all the work you put in to 14 | make my software available to a broader audience! I have tried to make PostSRSd 15 | easy to package, and this document is intended to document a few advanced CMake 16 | features you can use to adapt PostSRSd for your distribution. Feel free to open 17 | an issue if you think that PostSRSd can be improved. 18 | 19 | 20 | Third-Party Dependencies 21 | ------------------------ 22 | 23 | PostSRSd has gained a few external dependencies with its 2.0 rewrite, and it 24 | uses the CMake FetchContent_ module to manage those. By default, PostSRSd will 25 | download the sources of its dependencies at configure time, build them, and 26 | link them statically into the executable. 27 | 28 | Starting with CMake 3.24, it is possible to tweak this process and use system 29 | libraries by passing ``-DFETCHCONTENT_TRY_FIND_PACKAGE_MODE=ALWAYS`` to the 30 | CMake invocation. 31 | 32 | 33 | .. _FetchContent: https://cmake.org/cmake/help/latest/module/FetchContent.html 34 | 35 | 36 | Tweaking the Default Configuration 37 | ---------------------------------- 38 | 39 | PostSRSd mostly relies on the GNUInstallDirs_ module to discover the correct 40 | paths for data files. Additionally, PostSRSd has a few custom CMake options you 41 | can set with ``-D=`` and tweak the default settings for your 42 | users: 43 | 44 | - ``POSTSRSD_USER``: the unprivileged user as which PostSRSd is supposed to 45 | run. Defaults to ``nobody``. Note that this value can always be overridden in 46 | ``postsrsd.conf`` by the user. 47 | 48 | - ``POSTSRSD_CONFIGDIR``: the location where PostSRSd should store configuration 49 | files. The default is ``${CMAKE_INSTALL_FULL_SYSCONFDIR}``. 50 | 51 | - ``POSTSRSD_DATADIR``: the location where PostSRSd should store runtime files 52 | such as the SQLite database for envelope senders (if PostSRSd is configured 53 | to use that). The default is ``${CMAKE_INSTALL_LOCALSTATEDIR}/lib/postsrsd``. 54 | Note that this value can always be overridden in ``postsrsd.conf`` by the 55 | user. 56 | 57 | - ``POSTSRSD_CHROOTDIR``: the location where PostSRSd is supposed to jail 58 | itself. Defaults to ``${POSTSRSD_DATADIR}``. Note that this value can always 59 | be overridden in ``postsrsd.conf`` by the user. 60 | 61 | - ``GENERATE_SRS_SECRET``: If set to ``ON`` (the default), PostSRSd will create 62 | ``postsrsd.secret`` if it does not exist. This is helpful to make a source 63 | installation secure by default, but less so for a distributed binary package. 64 | 65 | - ``INSTALL_SYSTEMD_SERVICE``: If set to ``ON`` (the default), a postsrsd.service 66 | unit will be installed to allow starting PostSRSd via systemd. You can disable 67 | this if your distribution uses a different init system. 68 | 69 | - ``SYSTEMD_UNITDIR``: the intended install destination for the 70 | ``postsrsd.service`` file. The default should be fine for most systems, but 71 | you can override it if the auto-detected location is wrong. 72 | 73 | - ``SYSTEMD_SYSUSERSDIR``: the intended install destination for the 74 | sysusers.d configuration file. The default should be fine for most systems, but 75 | you can override it if the auto-detected location is wrong. 76 | 77 | - ``DEVELOPER_BUILD``: this makes the compiler treat all warnings as errors and 78 | enable as much of them as possible. While certainly useful for me as upstream 79 | developer (and for the Github CI), you should keep this option disabled. It 80 | will not do much for you except likely break your package build each time a 81 | new compiler version is released. 82 | 83 | 84 | .. _GNUInstallDirs: https://cmake.org/cmake/help/latest/module/GNUInstallDirs.html 85 | -------------------------------------------------------------------------------- /doc/postsrsd.conf: -------------------------------------------------------------------------------- 1 | # PostSRSd example configuration file 2 | # Copyright 2022-2023 Timo Röhling 3 | # SPDX-License-Identifier: FSFUL 4 | # 5 | # The copyright holder gives unlimited permission to copy, distribute and modify 6 | # this file. 7 | 8 | # Local domains 9 | # Your local domains need not be rewritten, so PostSRSd has to know about them. 10 | # 11 | # Example: 12 | # domains = { "example.com", "example.org", "example.net" } 13 | # 14 | domains = {} 15 | 16 | # Local domains (file storage) 17 | # Instead of listing your local domains directly, you can also write them to a 18 | # file and have PostSRSd read it. This is particularly useful if you have a 19 | # large number of domains for which you need to act as mail forwarder. PostSRSd 20 | # reads this file before it chroots and drops root privileges. The file format 21 | # is one domain per line. 22 | # 23 | # Example: 24 | # domains-file = "/usr/local/etc/postsrsd.domains" 25 | # 26 | #domains-file = 27 | 28 | # Dedicated SRS rewrite domain. 29 | # The local domain which is used to create the ephemeral SRS envelope 30 | # addresses. It is recommended that you use a dedicated mail domain for SRS if 31 | # you serve multiple unrelated domains (e.g. for your customers), to prevent 32 | # privacy issues. If unset, the first configured local domain is used. 33 | # 34 | # Example: 35 | # srs-domain = "srs.example.com" 36 | # 37 | #srs-domain = 38 | 39 | # Socketmap lookup table for Postfix integration. 40 | # Traditionally, PostSRSd interacts with Postfix through the canonicalization 41 | # lookup tables of the cleanup daemon. If you use a unix socket, be aware that 42 | # most Postfix instances will jail their cleanup daemon in a /var/spool/postfix 43 | # chroot, so no other path will be visible to them. Unix sockets are created 44 | # before PostSRSd chroots and drops root privileges. 45 | # 46 | # Examples: 47 | # socketmap = unix:/var/spool/postfix/srs 48 | # socketmap = inet:localhost:10003 49 | # 50 | socketmap = unix:/var/spool/postfix/srs 51 | 52 | # Socketmap connection keep-alive timeout. 53 | # After PostSRSd has served a socketmap request, it will keep the connection 54 | # open for a while longer, in case Postfix has additional queries. PostSRSd 55 | # will close the connection after the configured time (in seconds) has expired. 56 | # 57 | # Examples: 58 | # keep-alive = 30 59 | # 60 | keep-alive = 30 61 | 62 | # Milter endpoint for MTA integration. 63 | # PostSRSd can act as a milter to rewrite envelope addresses if it has been 64 | # built with milter support. Unix sockets are created before PostSRSd chroots 65 | # and drops root privileges. 66 | # 67 | # Examples: 68 | # milter = unix:/var/spool/postfix/srs_milter 69 | # milter = inet:localhost:9997 70 | # 71 | #milter = 72 | 73 | # Original envelope sender handling. 74 | # When the envelope sender is rewritten, the original address can either be 75 | # embedded in the rewritten address, or stored in a local database. Embedding 76 | # makes PostSRSd work fully stateless, but the full sender address needs to fit 77 | # into the localpart of the embedded address, effectively limiting the length 78 | # of forwardable sender addresses to 51 octets. Storing the sender address in a 79 | # database circumvents this problem, but makes PostSRSd vulnerable to an 80 | # attacker sending vast amounts of emails with fake sender addresses, all of 81 | # which need to be stored in the database. 82 | # 83 | # If you are unsure which option suits your use-case best, the vast majority of 84 | # mail addresses will be relatively short, so you should pick "embedded". 85 | # 86 | # Examples: 87 | # original-envelope = embedded 88 | # original-envelope = database 89 | # 90 | original-envelope = embedded 91 | 92 | # Database for envelope sender storage. 93 | # If you decide to store envelope senders in a database, this database will be 94 | # used. The option is ignored if original-envelope is set to "embedded". Also 95 | # note that PostSRSd needs to be built with SQLite or Redis support for this. 96 | # 97 | # PostSRSd reads this database after it chroots and drops root privileges, so 98 | # the actual filename is the chroot directory joined with this filename. 99 | # 100 | # Examples: 101 | # envelope-database = "sqlite:./senders.db" 102 | # envelope-database = "redis:localhost:6379" 103 | # 104 | #envelope-database = "sqlite:./senders.db" 105 | 106 | # Secret keys for signing and verifying SRS addresses. 107 | # Rewritten addresses are tagged with a truncated HMAC-SHA1 signature, to 108 | # prevent tampering and forged envelope addresses. You can have more than 109 | # one signing secret; each line of the secrets file is considered one secret 110 | # key. If an incoming signature matches any key, it is accepted. Outgoing 111 | # signatures will always be generated with the first configured secret. 112 | # 113 | # For security reasons, you should also make sure that the file is owned and 114 | # only accessible by root (chmod 600). PostSRSd reads this file before it 115 | # chroots and drops root privileges. 116 | # 117 | # Example: 118 | # secrets-file = "/usr/local/etc/postsrsd.secret" 119 | # 120 | secrets-file = "/usr/local/etc/postsrsd.secret" 121 | 122 | # SRS tag separator 123 | # This is the character following the initial SRS0 or SRS1 tag of a generated 124 | # sender address. Valid separators are "=", "+", and "-". Unless you have a 125 | # very good reason, you should leave this setting at its default. 126 | # 127 | separator = "=" 128 | 129 | # SRS hash signature length 130 | # Any SRS address will be signed with a truncated hash to prevent tampering and 131 | # ensure that only legitimate email bounces will be returned to sender. The 132 | # default length provides adequate security without taking up too much valuable 133 | # space. Unless you know what you are doing, you should leave this setting at 134 | # its default. 135 | # 136 | # WARNING: You can break your mail server (or worse, turn it into a spam relay) 137 | # if you mess up this setting. 138 | # 139 | hash-length = 4 140 | 141 | # SRS minimum acceptable hash signature length 142 | # This is the mininum signature length that PostSRSd considers valid. It is a 143 | # separate setting because if you decide to increase the hash length, you may 144 | # want to keep accepting the shorter hashes for a 24 hour grace period. Again, 145 | # Unless you know what you are doing, you should leave this setting at its 146 | # default. 147 | # 148 | # WARNING: You can break your mail server (or worse, turn it into a spam relay) 149 | # if you mess up this setting. 150 | # 151 | hash-minimum = 4 152 | 153 | # Always rewrite sender addresses 154 | # You can force PostSRSd to rewrite any sender address, even if it has been 155 | # rewritten already. You probably do not want to do this, though. 156 | # 157 | always-rewrite = off 158 | 159 | # Execute PostSRSd as unprivileged user 160 | # Drop root privileges and run as this user before entering the main loop and 161 | # handling untrusted input. To prevent PostSRSd from changing users, set this to 162 | # the empty string. 163 | # 164 | # Example: 165 | # unprivileged-user = "nobody" 166 | # 167 | unprivileged-user = "nobody" 168 | 169 | # Execute PostSRSd in chroot jail 170 | # PostSRSd will jail itself in the given directory, which adds an additional 171 | # layer of protection against the exploitation of security bugs in PostSRSd. To 172 | # prevent PostSRSd from chrooting, set this to the empty string. 173 | # 174 | # Example: 175 | # chroot-dir = "/usr/local/var/lib/postsrsd" 176 | # 177 | chroot-dir = "/usr/local/var/lib/postsrsd" 178 | 179 | # Syslog 180 | # PostSRSd writes log messages to stderr. If you enable this option, PostSRSd 181 | # will also send all messages to the syslog mail facility. 182 | # 183 | syslog = off 184 | 185 | # Debug 186 | # This option makes PostSRSd more verbose in its logging, which can be useful 187 | # to hunt down configuration problems. 188 | # 189 | debug = off 190 | -------------------------------------------------------------------------------- /src/config.c: -------------------------------------------------------------------------------- 1 | /* PostSRSd - Sender Rewriting Scheme daemon for Postfix 2 | * Copyright 2012-2023 Timo Röhling 3 | * SPDX-License-Identifier: GPL-3.0-only 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | #include "config.h" 19 | 20 | #include "postsrsd_build_config.h" 21 | #include "util.h" 22 | 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | 30 | #ifndef HAVE_STRCASECMP 31 | # ifdef HAVE__STRICMP 32 | # define strcasecmp _stricmp 33 | # endif 34 | #endif 35 | 36 | static int parse_original_envelope(cfg_t* cfg, cfg_opt_t* opt, 37 | const char* value, void* result) 38 | { 39 | if (strcasecmp(value, "embedded") == 0) 40 | *(long*)result = SRS_ENVELOPE_EMBEDDED; 41 | else if (strcasecmp(value, "database") == 0) 42 | *(long*)result = SRS_ENVELOPE_DATABASE; 43 | else 44 | { 45 | cfg_error(cfg, "option '%s' must be either 'embedded' or 'database'", 46 | cfg_opt_name(opt)); 47 | return -1; 48 | } 49 | return 0; 50 | } 51 | 52 | static int validate_separator(cfg_t* cfg, cfg_opt_t* opt) 53 | { 54 | const char* value = cfg_opt_getstr(opt); 55 | if (strlen(value) != 1 || !strpbrk(value, "=+-")) 56 | { 57 | cfg_error(cfg, "option '%s' must be one of '=', '+', '-'", 58 | cfg_opt_name(opt)); 59 | return -1; 60 | } 61 | return 0; 62 | } 63 | 64 | static int validate_uint(cfg_t* cfg, cfg_opt_t* opt) 65 | { 66 | int value = cfg_opt_getnint(opt, cfg_opt_size(opt) - 1); 67 | if (value < 0) 68 | { 69 | cfg_error(cfg, "option '%s' must be non-negative", cfg_opt_name(opt)); 70 | return -1; 71 | } 72 | return 0; 73 | } 74 | 75 | static bool is_valid_domain_name(const char* s) 76 | { 77 | char prev = 0; 78 | if (s == NULL) 79 | return false; 80 | if (*s == 0) 81 | return false; 82 | while (*s != 0) 83 | { 84 | if (*s == '.' && prev == '.') 85 | return false; 86 | if (!isalnum(*s) && *s != '-' && *s != '.') 87 | return false; 88 | prev = *s++; 89 | } 90 | return prev != '.'; 91 | } 92 | 93 | static int validate_domain_names(cfg_t* cfg, cfg_opt_t* opt) 94 | { 95 | unsigned ndomains = cfg_opt_size(opt); 96 | for (unsigned i = 0; i < ndomains; ++i) 97 | { 98 | const char* domain = cfg_opt_getnstr(opt, i); 99 | if (!is_valid_domain_name(domain)) 100 | { 101 | cfg_error(cfg, "option '%s' has invalid domain name '%s'", 102 | cfg_opt_name(opt), domain); 103 | return -1; 104 | } 105 | } 106 | return 0; 107 | } 108 | 109 | static void show_help() 110 | { 111 | puts( 112 | "PostSRSd - Sender Rewriting Scheme daemon for Postfix\n" 113 | "\n" 114 | "Available command line options:\n" 115 | " -h show this help\n" 116 | " -C load configuration from \n" 117 | " (default: " DEFAULT_CONFIG_FILE 118 | ")\n" 119 | " -c use as chroot directory\n" 120 | " (default: " DEFAULT_CHROOT_DIR 121 | ")\n" 122 | " -D daemonize by forking into background\n" 123 | " -p write PostSRSd process ID into \n" 124 | " -u drop root privileges and run as \n" 125 | " (default: " DEFAULT_POSTSRSD_USER 126 | ")\n" 127 | " -v show version number (" POSTSRSD_VERSION 128 | ")\n" 129 | #if defined(WITH_SQLITE) || defined(WITH_REDIS) || defined(WITH_MILTER) 130 | "\n" 131 | "This binary has been compiled with\n" 132 | # ifdef WITH_SQLITE 133 | "* SQLite database storage support\n" 134 | # endif 135 | # ifdef WITH_REDIS 136 | "* Redis database storage support\n" 137 | # endif 138 | # ifdef WITH_MILTER 139 | "* Milter support (experimental)\n" 140 | # endif 141 | #endif 142 | ); 143 | } 144 | 145 | cfg_t* config_defaults() 146 | { 147 | static cfg_opt_t opts[] = { 148 | CFG_STR("srs-domain", NULL, CFGF_NODEFAULT), 149 | CFG_STR_LIST("domains", "{}", CFGF_NONE), 150 | CFG_STR("domains-file", NULL, CFGF_NODEFAULT), 151 | CFG_INT_CB("original-envelope", SRS_ENVELOPE_EMBEDDED, CFGF_NONE, 152 | parse_original_envelope), 153 | CFG_STR("separator", "=", CFGF_NONE), 154 | CFG_INT("hash-length", 4, CFGF_NONE), 155 | CFG_INT("hash-minimum", 4, CFGF_NONE), 156 | CFG_BOOL("always-rewrite", cfg_false, CFGF_NONE), 157 | CFG_STR("socketmap", "unix:/var/spool/postfix/srs", CFGF_NONE), 158 | CFG_INT("keep-alive", 30, CFGF_NONE), 159 | CFG_STR("milter", NULL, CFGF_NODEFAULT), 160 | CFG_STR("secrets-file", DEFAULT_SECRETS_FILE, CFGF_NONE), 161 | CFG_STR("envelope-database", NULL, CFGF_NODEFAULT), 162 | CFG_STR("pid-file", NULL, CFGF_NODEFAULT), 163 | CFG_STR("unprivileged-user", DEFAULT_POSTSRSD_USER, CFGF_NONE), 164 | CFG_STR("chroot-dir", DEFAULT_CHROOT_DIR, CFGF_NONE), 165 | CFG_BOOL("daemonize", cfg_false, CFGF_NONE), 166 | CFG_BOOL("syslog", cfg_false, CFGF_NONE), 167 | CFG_BOOL("debug", cfg_false, CFGF_NONE), 168 | CFG_END(), 169 | }; 170 | cfg_t* cfg = cfg_init(opts, CFGF_NONE); 171 | cfg_set_validate_func(cfg, "separator", validate_separator); 172 | cfg_set_validate_func(cfg, "srs-domain", validate_domain_names); 173 | cfg_set_validate_func(cfg, "domains", validate_domain_names); 174 | cfg_set_validate_func(cfg, "keep-alive", validate_uint); 175 | return cfg; 176 | } 177 | 178 | cfg_t* config_from_commandline(int argc, char* const* argv) 179 | { 180 | cfg_t* cfg = config_defaults(); 181 | int opt; 182 | char* config_file = NULL; 183 | char* pid_file = NULL; 184 | char* chroot_dir = NULL; 185 | char* unprivileged_user = NULL; 186 | int daemonize = 0; 187 | int ok = 1; 188 | if (file_exists(DEFAULT_CONFIG_FILE)) 189 | set_string(&config_file, strdup(DEFAULT_CONFIG_FILE)); 190 | while ((opt = getopt(argc, argv, "C:c:Dhp:u:v")) != -1) 191 | { 192 | switch (opt) 193 | { 194 | case '?': 195 | return 0; 196 | case 'C': 197 | set_string(&config_file, strdup(optarg)); 198 | break; 199 | case 'c': 200 | set_string(&chroot_dir, strdup(optarg)); 201 | break; 202 | case 'D': 203 | daemonize = 1; 204 | break; 205 | case 'h': 206 | show_help(); 207 | exit(0); 208 | break; 209 | case 'p': 210 | set_string(&pid_file, strdup(optarg)); 211 | break; 212 | case 'u': 213 | set_string(&unprivileged_user, strdup(optarg)); 214 | break; 215 | case 'v': 216 | puts(POSTSRSD_VERSION); 217 | exit(0); 218 | break; 219 | default: 220 | break; 221 | } 222 | } 223 | if (config_file) 224 | { 225 | switch (cfg_parse(cfg, config_file)) 226 | { 227 | case CFG_FILE_ERROR: 228 | log_error("cannot read '%s': %s", config_file, strerror(errno)); 229 | ok = 0; 230 | break; 231 | case CFG_PARSE_ERROR: 232 | log_error("malformed configuration file '%s'", config_file); 233 | ok = 0; 234 | break; 235 | default: 236 | break; 237 | } 238 | set_string(&config_file, NULL); 239 | } 240 | if (pid_file) 241 | { 242 | cfg_setstr(cfg, "pid-file", pid_file); 243 | set_string(&pid_file, NULL); 244 | } 245 | if (unprivileged_user) 246 | { 247 | cfg_setstr(cfg, "unprivileged-user", unprivileged_user); 248 | set_string(&unprivileged_user, NULL); 249 | } 250 | if (chroot_dir) 251 | { 252 | cfg_setstr(cfg, "chroot-dir", chroot_dir); 253 | set_string(&chroot_dir, NULL); 254 | } 255 | if (daemonize) 256 | cfg_setbool(cfg, "daemonize", cfg_true); 257 | if (ok) 258 | return cfg; 259 | cfg_free(cfg); 260 | return NULL; 261 | } 262 | 263 | srs_t* srs_from_config(cfg_t* cfg) 264 | { 265 | srs_t* srs = srs_new(); 266 | srs_set_alwaysrewrite(srs, cfg_getbool(cfg, "always-rewrite")); 267 | srs_set_hashlength(srs, cfg_getint(cfg, "hash-length")); 268 | srs_set_hashmin(srs, cfg_getint(cfg, "hash-minimum")); 269 | srs_set_separator(srs, cfg_getstr(cfg, "separator")[0]); 270 | char* secrets_file = cfg_getstr(cfg, "secrets-file"); 271 | if (secrets_file && secrets_file[0]) 272 | { 273 | FILE* f = fopen(secrets_file, "r"); 274 | if (f) 275 | { 276 | char buffer[1024]; 277 | char* secret; 278 | while ((secret = fgets(buffer, sizeof(buffer), f)) != NULL) 279 | { 280 | secret = strtok(secret, "\r\n"); 281 | if (secret && secret[0]) 282 | srs_add_secret(srs, secret); 283 | } 284 | fclose(f); 285 | } 286 | else 287 | { 288 | log_error("cannot read secrets from %s", secrets_file); 289 | srs_free(srs); 290 | return NULL; 291 | } 292 | } 293 | if (srs->numsecrets == 0 || srs->secrets == NULL || srs->secrets[0] == NULL) 294 | { 295 | log_error("need at least one secret"); 296 | srs_free(srs); 297 | return NULL; 298 | } 299 | char* faketime = getenv("POSTSRSD_FAKETIME"); 300 | if (faketime) 301 | { 302 | char* eptr; 303 | long stamp = strtol(faketime, &eptr, 10); 304 | if (eptr && *eptr == 0) 305 | { 306 | srs->faketime = stamp; 307 | log_warn( 308 | "POSTSRSD_FAKETIME=%s overrides system clock. DO NOT USE IN " 309 | "PRODUCTION!", 310 | faketime); 311 | } 312 | else 313 | { 314 | log_error("POSTSRSD_FAKETIME must be an integer"); 315 | srs_free(srs); 316 | return NULL; 317 | } 318 | } 319 | return srs; 320 | } 321 | 322 | bool srs_domains_from_config(cfg_t* cfg, char** srs_domain, 323 | struct domain_set** local_domains) 324 | { 325 | *srs_domain = NULL; 326 | *local_domains = domain_set_create(); 327 | char* domain; 328 | domain = cfg_getstr(cfg, "srs-domain"); 329 | if (domain && domain[0]) 330 | *srs_domain = strdup(domain[0] == '.' ? domain + 1 : domain); 331 | unsigned ndomains = cfg_size(cfg, "domains"); 332 | for (unsigned i = 0; i < ndomains; ++i) 333 | { 334 | domain = cfg_getnstr(cfg, "domains", i); 335 | if (domain && domain[0]) 336 | { 337 | domain_set_add(*local_domains, domain); 338 | if (*srs_domain == NULL) 339 | *srs_domain = strdup(domain[0] == '.' ? domain + 1 : domain); 340 | } 341 | } 342 | char* domains_file = cfg_getstr(cfg, "domains-file"); 343 | if (domains_file && domains_file[0]) 344 | { 345 | FILE* f = fopen(domains_file, "r"); 346 | if (f) 347 | { 348 | char buffer[1024]; 349 | char* end; 350 | while ((domain = fgets(buffer, sizeof(buffer), f)) != NULL) 351 | { 352 | domain = strtok(domain, "\r\n"); 353 | if (domain == NULL) 354 | continue; 355 | while (isspace(domain[0])) 356 | ++domain; 357 | end = strchr(domain, '#'); 358 | if (end != NULL) 359 | *end = 0; 360 | end = domain + strlen(domain); 361 | while (end != domain && isspace(*(end - 1))) 362 | *--end = 0; 363 | if (domain[0] == 0) 364 | continue; 365 | if (is_valid_domain_name(domain)) 366 | { 367 | domain_set_add(*local_domains, domain); 368 | if (*srs_domain == NULL) 369 | *srs_domain = 370 | strdup(domain[0] == '.' ? domain + 1 : domain); 371 | } 372 | else 373 | { 374 | log_error("invalid domain name '%s' in domains file", 375 | domain); 376 | goto fail; 377 | } 378 | } 379 | fclose(f); 380 | } 381 | else 382 | { 383 | log_error("cannot read local domains from %s", domains_file); 384 | goto fail; 385 | } 386 | } 387 | return true; 388 | fail: 389 | domain_set_destroy(*local_domains); 390 | *local_domains = NULL; 391 | free(*srs_domain); 392 | *srs_domain = NULL; 393 | return false; 394 | } 395 | -------------------------------------------------------------------------------- /src/config.h: -------------------------------------------------------------------------------- 1 | /* PostSRSd - Sender Rewriting Scheme daemon for Postfix 2 | * Copyright 2012-2022 Timo Röhling 3 | * SPDX-License-Identifier: GPL-3.0-only 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #ifndef CONFIG_H 18 | #define CONFIG_H 19 | 20 | #define SRS_ENVELOPE_EMBEDDED 0 21 | #define SRS_ENVELOPE_DATABASE 1 22 | 23 | #include "srs2.h" 24 | #include "util.h" 25 | 26 | #include 27 | 28 | cfg_t* config_defaults(); 29 | cfg_t* config_from_commandline(int argc, char* const* argv); 30 | srs_t* srs_from_config(cfg_t* cfg); 31 | bool srs_domains_from_config(cfg_t* cfg, char** srs_domain, 32 | struct domain_set** other_domains); 33 | #endif 34 | -------------------------------------------------------------------------------- /src/database.c: -------------------------------------------------------------------------------- 1 | /* PostSRSd - Sender Rewriting Scheme daemon for Postfix 2 | * Copyright 2012-2023 Timo Röhling 3 | * SPDX-License-Identifier: GPL-3.0-only 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #include "database.h" 18 | 19 | #include "postsrsd_build_config.h" 20 | #include "util.h" 21 | 22 | #include 23 | #include 24 | #ifdef WITH_REDIS 25 | # include 26 | #endif 27 | #ifdef WITH_SQLITE 28 | # include 29 | #endif 30 | #ifdef HAVE_SYS_TIME_H 31 | # include 32 | #endif 33 | #ifdef HAVE_TIME_H 34 | # include 35 | #endif 36 | 37 | struct database 38 | { 39 | char* (*read)(database_t*, const char*); 40 | bool (*write)(database_t*, const char*, const char*, unsigned); 41 | void (*expire)(database_t*); 42 | void (*disconnect)(database_t*); 43 | void* handle; 44 | #ifdef WITH_SQLITE 45 | sqlite3_stmt *read_stmt, *write_stmt, *expire_stmt; 46 | #endif 47 | }; 48 | 49 | #ifdef WITH_SQLITE 50 | static char* db_sqlite_read(database_t* db, const char* key) 51 | { 52 | if (db->read_stmt == NULL) 53 | { 54 | sqlite3* handle = (sqlite3*)db->handle; 55 | if (sqlite3_prepare_v2(handle, "SELECT v FROM kv WHERE k = ?", -1, 56 | &db->read_stmt, NULL) 57 | != SQLITE_OK) 58 | { 59 | log_error("failed to prepare sqlite read statement"); 60 | return NULL; 61 | } 62 | } 63 | char* value = NULL; 64 | sqlite3_bind_text(db->read_stmt, 1, key, -1, SQLITE_STATIC); 65 | int result = sqlite3_step(db->read_stmt); 66 | if (result == SQLITE_ERROR) 67 | { 68 | sqlite3* handle = (sqlite3*)db->handle; 69 | log_warn("sqlite read error: %s", sqlite3_errmsg(handle)); 70 | } 71 | if (result == SQLITE_ROW) 72 | { 73 | value = strdup((const char*)sqlite3_column_text(db->read_stmt, 0)); 74 | } 75 | sqlite3_reset(db->read_stmt); 76 | sqlite3_clear_bindings(db->read_stmt); 77 | return value; 78 | } 79 | 80 | static bool db_sqlite_write(database_t* db, const char* key, const char* value, 81 | unsigned lifetime) 82 | { 83 | if (db->write_stmt == NULL) 84 | { 85 | sqlite3* handle = (sqlite3*)db->handle; 86 | if (sqlite3_prepare_v2(handle, 87 | "INSERT INTO kv (k, v, lt) VALUES (?, ?, ?)", -1, 88 | &db->write_stmt, NULL) 89 | != SQLITE_OK) 90 | { 91 | log_error("failed to prepare sqlite write statement"); 92 | return false; 93 | } 94 | } 95 | bool success = true; 96 | sqlite3_bind_text(db->write_stmt, 1, key, -1, SQLITE_STATIC); 97 | sqlite3_bind_text(db->write_stmt, 2, value, -1, SQLITE_STATIC); 98 | sqlite3_bind_int64(db->write_stmt, 3, time(NULL) + lifetime); 99 | if (sqlite3_step(db->write_stmt) == SQLITE_ERROR) 100 | { 101 | sqlite3* handle = (sqlite3*)db->handle; 102 | log_warn("sqlite write error: %s", sqlite3_errmsg(handle)); 103 | success = false; 104 | } 105 | sqlite3_reset(db->write_stmt); 106 | sqlite3_clear_bindings(db->write_stmt); 107 | return success; 108 | } 109 | 110 | static void db_sqlite_expire(database_t* db) 111 | { 112 | if (db->expire_stmt == NULL) 113 | { 114 | sqlite3* handle = (sqlite3*)db->handle; 115 | if (sqlite3_prepare_v2(handle, "DELETE FROM kv WHERE lt <= ?", -1, 116 | &db->expire_stmt, NULL) 117 | != SQLITE_OK) 118 | { 119 | log_error("failed to prepare sqlite expire statement"); 120 | return; 121 | } 122 | } 123 | sqlite3_bind_int64(db->expire_stmt, 1, time(NULL)); 124 | sqlite3_step(db->expire_stmt); 125 | sqlite3_reset(db->expire_stmt); 126 | } 127 | 128 | static void db_sqlite_disconnect(database_t* db) 129 | { 130 | sqlite3* handle = (sqlite3*)db->handle; 131 | sqlite3_finalize(db->expire_stmt); 132 | sqlite3_finalize(db->read_stmt); 133 | sqlite3_finalize(db->write_stmt); 134 | sqlite3_close(handle); 135 | } 136 | 137 | static bool db_sqlite_connect(database_t* db, const char* uri, 138 | bool create_if_not_exist) 139 | { 140 | sqlite3* handle; 141 | char* err; 142 | if (sqlite3_open(uri, &handle) != SQLITE_OK) 143 | { 144 | sqlite3_close(handle); 145 | return false; 146 | } 147 | if (create_if_not_exist) 148 | { 149 | if (sqlite3_exec(handle, 150 | "CREATE TABLE IF NOT EXISTS kv (" 151 | "k TEXT NOT NULL UNIQUE ON CONFLICT REPLACE," 152 | "v TEXT NOT NULL," 153 | "lt INTEGER NOT NULL);" 154 | "CREATE INDEX IF NOT EXISTS ltidx ON kv (lt)", 155 | NULL, NULL, &err) 156 | != SQLITE_OK) 157 | { 158 | if (err != NULL) 159 | { 160 | log_error("%s", err); 161 | sqlite3_free(err); 162 | } 163 | sqlite3_close(handle); 164 | return false; 165 | } 166 | } 167 | db->handle = handle; 168 | db->read = db_sqlite_read; 169 | db->write = db_sqlite_write; 170 | db->expire = db_sqlite_expire; 171 | db->disconnect = db_sqlite_disconnect; 172 | db->read_stmt = NULL; 173 | db->write_stmt = NULL; 174 | db->expire_stmt = NULL; 175 | return true; 176 | } 177 | #endif 178 | 179 | #ifdef WITH_REDIS 180 | static char* db_redis_read(database_t* db, const char* key) 181 | { 182 | char buffer[128]; 183 | snprintf(buffer, sizeof(buffer), "PostSRSd/%s", key); 184 | redisContext* handle = (redisContext*)db->handle; 185 | redisReply* reply = redisCommand(handle, "GET %s", buffer); 186 | if (reply == NULL) 187 | { 188 | log_warn("redis connection failure: %s", handle->errstr); 189 | return NULL; 190 | } 191 | char* value = NULL; 192 | if (reply->type == REDIS_REPLY_ERROR) 193 | { 194 | log_warn("redis read error: %s", reply->str); 195 | } 196 | if (reply->type == REDIS_REPLY_STRING) 197 | { 198 | value = strdup(reply->str); 199 | } 200 | freeReplyObject(reply); 201 | return value; 202 | } 203 | 204 | static bool db_redis_write(database_t* db, const char* key, const char* value, 205 | unsigned lifetime) 206 | { 207 | char buffer[128]; 208 | snprintf(buffer, sizeof(buffer), "PostSRSd/%s", key); 209 | redisContext* handle = (redisContext*)db->handle; 210 | bool success = true; 211 | redisReply* reply = 212 | redisCommand(handle, "SETEX %s %u %s", buffer, lifetime, value); 213 | if (reply == NULL) 214 | { 215 | log_warn("redis connection failure: %s", handle->errstr); 216 | return false; 217 | } 218 | if (reply->type == REDIS_REPLY_ERROR) 219 | { 220 | log_warn("redis write error: %s", reply->str); 221 | success = false; 222 | } 223 | freeReplyObject(reply); 224 | return success; 225 | } 226 | 227 | static void db_redis_disconnect(database_t* db) 228 | { 229 | redisContext* handle = (redisContext*)db->handle; 230 | redisFree(handle); 231 | } 232 | 233 | static bool db_redis_connect(database_t* db, const char* hostname, int port) 234 | { 235 | redisContext* handle; 236 | if (port > 0) 237 | { 238 | handle = redisConnect(hostname, port); 239 | if (handle == NULL) 240 | goto alloc_fail; 241 | if (handle->err) 242 | goto conn_fail; 243 | redisEnableKeepAlive(handle); 244 | } 245 | else 246 | { 247 | handle = redisConnectUnix(hostname); 248 | if (handle == NULL) 249 | goto alloc_fail; 250 | } 251 | if (handle->err) 252 | goto conn_fail; 253 | db->handle = handle; 254 | db->read = db_redis_read; 255 | db->write = db_redis_write; 256 | db->expire = NULL; 257 | db->disconnect = db_redis_disconnect; 258 | return true; 259 | 260 | conn_fail: 261 | log_error("failed to connect to redis instance: %s", handle->errstr); 262 | redisFree(handle); 263 | return false; 264 | 265 | alloc_fail: 266 | log_error("failed to allocate redis handle"); 267 | return false; 268 | } 269 | #endif 270 | 271 | database_t* database_connect(const char* uri, bool create_if_not_exist) 272 | { 273 | MAYBE_UNUSED(create_if_not_exist); 274 | if (NULL_OR_EMPTY_STRING(uri)) 275 | { 276 | log_error("not database uri configured"); 277 | return NULL; 278 | } 279 | #ifdef WITH_SQLITE 280 | if (strncmp(uri, "sqlite:", 7) == 0) 281 | { 282 | database_t* db = (database_t*)malloc(sizeof(struct database)); 283 | if (db == NULL) 284 | { 285 | log_error("failed to allocate database connection handle"); 286 | return NULL; 287 | } 288 | if (!db_sqlite_connect(db, uri + 7, create_if_not_exist)) 289 | { 290 | log_error("failed to connect to '%s'", uri); 291 | free(db); 292 | return NULL; 293 | } 294 | return db; 295 | } 296 | #endif 297 | #ifdef WITH_REDIS 298 | if (strncmp(uri, "redis:", 6) == 0) 299 | { 300 | database_t* db = (database_t*)malloc(sizeof(struct database)); 301 | if (db == NULL) 302 | { 303 | log_error("failed to allocate database connection handle"); 304 | return NULL; 305 | } 306 | int port; 307 | char* hostname = endpoint_for_redis(&uri[6], &port); 308 | if (hostname == NULL) 309 | { 310 | log_error("invalid database uri '%s'", uri); 311 | free(db); 312 | return NULL; 313 | } 314 | if (!db_redis_connect(db, hostname, port)) 315 | { 316 | log_error("failed to connect to '%s'", uri); 317 | free(hostname); 318 | free(db); 319 | return NULL; 320 | } 321 | free(hostname); 322 | return db; 323 | } 324 | #endif 325 | log_error("unsupported database '%s'", uri); 326 | return NULL; 327 | } 328 | 329 | char* database_read(database_t* db, const char* key) 330 | { 331 | if (db != NULL && key != NULL) 332 | return db->read(db, key); 333 | return NULL; 334 | } 335 | 336 | bool database_write(database_t* db, const char* key, const char* value, 337 | unsigned lifetime) 338 | { 339 | if (db != NULL && key != NULL && value != NULL) 340 | return db->write(db, key, value, lifetime); 341 | return false; 342 | } 343 | 344 | void database_expire(database_t* db) 345 | { 346 | if (db != NULL && db->expire != NULL) 347 | db->expire(db); 348 | } 349 | 350 | void database_disconnect(database_t* db) 351 | { 352 | if (db != NULL) 353 | db->disconnect(db); 354 | free(db); 355 | } 356 | -------------------------------------------------------------------------------- /src/database.h: -------------------------------------------------------------------------------- 1 | /* PostSRSd - Sender Rewriting Scheme daemon for Postfix 2 | * Copyright 2012-2022 Timo Röhling 3 | * SPDX-License-Identifier: GPL-3.0-only 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #ifndef DATABASE_H 18 | #define DATABASE_H 19 | 20 | #include 21 | 22 | struct database; 23 | typedef struct database database_t; 24 | 25 | database_t* database_connect(const char* uri, bool create_if_not_exist); 26 | char* database_read(database_t* db, const char* key); 27 | bool database_write(database_t* db, const char* key, const char* value, 28 | unsigned lifetime); 29 | void database_expire(database_t* db); 30 | void database_disconnect(database_t* db); 31 | 32 | #endif 33 | -------------------------------------------------------------------------------- /src/endpoint.c: -------------------------------------------------------------------------------- 1 | /* PostSRSd - Sender Rewriting Scheme daemon for Postfix 2 | * Copyright 2012-2023 Timo Röhling 3 | * SPDX-License-Identifier: GPL-3.0-only 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #include "endpoint.h" 18 | 19 | #include "postsrsd_build_config.h" 20 | #include "util.h" 21 | 22 | #include 23 | #include 24 | #ifdef HAVE_ERRNO_H 25 | # include 26 | #endif 27 | #ifdef HAVE_SYS_TYPES_H 28 | # include 29 | #endif 30 | #ifdef HAVE_SYS_SOCKET_H 31 | # include 32 | #endif 33 | #ifdef HAVE_SYS_STAT_H 34 | # include 35 | #endif 36 | #ifdef HAVE_SYS_UN_H 37 | # include 38 | #endif 39 | #ifdef HAVE_NETDB_H 40 | # include 41 | #endif 42 | #ifdef HAVE_UNISTD_H 43 | # include 44 | #endif 45 | 46 | #if defined(AF_UNIX) 47 | # define HAVE_UNIX_SOCKETS 1 48 | #endif 49 | #if defined(HAVE_NETDB_H) && defined(AF_UNSPEC) && defined(AF_INET) \ 50 | && defined(AF_INET6) 51 | # define HAVE_INET_SOCKETS 1 52 | #endif 53 | #ifndef SO_REUSEPORT 54 | # define SO_REUSEPORT SO_REUSEADDR 55 | #endif 56 | #define POSTSRSD_SOCKET_LISTEN_QUEUE 16 57 | 58 | #ifdef HAVE_UNIX_SOCKETS 59 | static int create_unix_socket(const char* path) 60 | { 61 | struct sockaddr_un sa; 62 | if (NULL_OR_EMPTY_STRING(path)) 63 | { 64 | log_error("expected file path for unix socket"); 65 | return -1; 66 | } 67 | if (acquire_lock(path) > 0) 68 | unlink(path); 69 | int sock = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); 70 | if (sock < 0) 71 | goto fail; 72 | sa.sun_family = AF_UNIX; 73 | memset(sa.sun_path, 0, sizeof(sa.sun_path)); 74 | strncpy(sa.sun_path, path, sizeof(sa.sun_path) - 1); 75 | if (bind(sock, (const struct sockaddr*)&sa, sizeof(struct sockaddr_un)) < 0) 76 | goto fail; 77 | if (chmod(path, 0666) < 0) 78 | goto fail; 79 | if (listen(sock, POSTSRSD_SOCKET_LISTEN_QUEUE) < 0) 80 | goto fail; 81 | return sock; 82 | fail: 83 | log_perror(errno, NULL); 84 | if (sock >= 0) 85 | close(sock); 86 | return -1; 87 | } 88 | #endif 89 | 90 | #ifdef HAVE_INET_SOCKETS 91 | static int create_inet_sockets(char* addr, int family, int max_fds, int* fds) 92 | { 93 | const int one = 1; 94 | struct addrinfo hints, *ai; 95 | memset(&hints, 0, sizeof(struct addrinfo)); 96 | char* node = addr; 97 | char* service = NULL; 98 | if (addr[0] == '[') 99 | { 100 | node = ++addr; 101 | while (*addr != ']') 102 | { 103 | if (*addr == 0) 104 | { 105 | log_error("expected closing ']' in socket address"); 106 | return -1; 107 | } 108 | ++addr; 109 | } 110 | *addr++ = 0; 111 | if (*addr != ':') 112 | { 113 | log_error("expected ':' separator in socket address"); 114 | return -1; 115 | } 116 | service = ++addr; 117 | } 118 | else 119 | { 120 | service = strchr(addr, ':'); 121 | if (service) 122 | { 123 | *service = 0; 124 | ++service; 125 | } 126 | else 127 | { 128 | service = addr; 129 | node = NULL; 130 | } 131 | } 132 | if (NULL_OR_EMPTY_STRING(service)) 133 | { 134 | log_error("expected portnumber in socket address"); 135 | return -1; 136 | } 137 | hints.ai_family = family; 138 | hints.ai_socktype = SOCK_STREAM; 139 | if (node != NULL && strcmp(node, "*") == 0) 140 | { 141 | node = NULL; 142 | hints.ai_flags |= AI_PASSIVE; 143 | } 144 | if (node != NULL && strcmp(node, "localhost") == 0) 145 | { 146 | node = NULL; 147 | } 148 | int err = getaddrinfo(node, service, &hints, &ai); 149 | if (err != 0) 150 | { 151 | log_error("%s", gai_strerror(err)); 152 | return -1; 153 | } 154 | int sock = -1, count = 0; 155 | for (struct addrinfo* it = ai; it; it = it->ai_next) 156 | { 157 | if (max_fds == 0) 158 | break; 159 | sock = socket(it->ai_family, 160 | it->ai_socktype | SOCK_NONBLOCK | SOCK_CLOEXEC, 161 | it->ai_protocol); 162 | if (sock < 0) 163 | goto fail; 164 | if (setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one)) < 0) 165 | goto fail; 166 | if (bind(sock, it->ai_addr, it->ai_addrlen) < 0) 167 | goto fail; 168 | if (listen(sock, POSTSRSD_SOCKET_LISTEN_QUEUE) < 0) 169 | goto fail; 170 | *fds++ = sock; 171 | max_fds--; 172 | count++; 173 | continue; 174 | fail: 175 | err = errno; 176 | log_perror(err, NULL); 177 | if (sock >= 0) 178 | close(sock); 179 | } 180 | freeaddrinfo(ai); 181 | if (count == 0 && err != 0) 182 | return -1; 183 | return count; 184 | } 185 | #endif 186 | 187 | int endpoint_create(const char* s, int max_fds, int* fds) 188 | { 189 | MAYBE_UNUSED(fds); 190 | if (max_fds < 1) 191 | return 0; 192 | #ifdef HAVE_UNIX_SOCKETS 193 | const char* path = NULL; 194 | if (strncmp(s, "unix:", 5) == 0) 195 | { 196 | path = &s[5]; 197 | } 198 | else if (strncmp(s, "local:", 6) == 0) 199 | { 200 | path = &s[6]; 201 | } 202 | if (path) 203 | { 204 | int fd = create_unix_socket(path); 205 | if (fd < 0) 206 | { 207 | log_error("failed to create endpoint '%s'", s); 208 | return -1; 209 | } 210 | *fds = fd; 211 | return 1; 212 | } 213 | #endif 214 | #ifdef HAVE_INET_SOCKETS 215 | char* addr = NULL; 216 | int family = AF_UNSPEC; 217 | if (strncmp(s, "inet:", 5) == 0) 218 | { 219 | addr = strdup(&s[5]); 220 | } 221 | else if (strncmp(s, "inet4:", 6) == 0) 222 | { 223 | addr = strdup(&s[6]); 224 | family = AF_INET; 225 | } 226 | else if (strncmp(s, "inet6:", 6) == 0) 227 | { 228 | addr = strdup(&s[6]); 229 | family = AF_INET6; 230 | } 231 | if (addr) 232 | { 233 | int ret = create_inet_sockets(addr, family, max_fds, fds); 234 | free(addr); 235 | if (ret < 0) 236 | { 237 | log_error("failed to create endpoint '%s'", s); 238 | } 239 | return ret; 240 | } 241 | #endif 242 | log_error("unsupported endpoint '%s'", s); 243 | return -1; 244 | } 245 | -------------------------------------------------------------------------------- /src/endpoint.h: -------------------------------------------------------------------------------- 1 | /* PostSRSd - Sender Rewriting Scheme daemon for Postfix 2 | * Copyright 2012-2022 Timo Röhling 3 | * SPDX-License-Identifier: GPL-3.0-only 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #ifndef ENDPOINT_H 18 | #define ENDPOINT_H 19 | 20 | int endpoint_create(const char* s, int max_fd, int* fds); 21 | 22 | #endif 23 | -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | /* PostSRSd - Sender Rewriting Scheme daemon for Postfix 2 | * Copyright 2012-2023 Timo Röhling 3 | * SPDX-License-Identifier: GPL-3.0-only 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #include "config.h" 18 | #include "database.h" 19 | #include "endpoint.h" 20 | #include "milter.h" 21 | #include "netstring.h" 22 | #include "postsrsd_build_config.h" 23 | #include "srs.h" 24 | #include "util.h" 25 | 26 | #include 27 | #include 28 | #include 29 | #ifdef HAVE_FCNTL_H 30 | # include 31 | #endif 32 | #ifdef HAVE_ERRNO_H 33 | # include 34 | #endif 35 | #ifdef HAVE_SYS_SOCKET_H 36 | # include 37 | #endif 38 | #ifdef HAVE_SYS_TYPES_H 39 | # include 40 | #endif 41 | #ifdef HAVE_SYS_WAIT_H 42 | # include 43 | #endif 44 | #ifdef HAVE_SIGNAL_H 45 | # include 46 | #endif 47 | #ifdef HAVE_PWD_H 48 | # include 49 | #endif 50 | #ifdef HAVE_POLL_H 51 | # include 52 | #endif 53 | #ifdef HAVE_UNISTD_H 54 | # include 55 | #endif 56 | #ifdef HAVE_GRP_H 57 | # include 58 | #endif 59 | 60 | static volatile sig_atomic_t timeout = 0; 61 | 62 | static bool drop_privileges(cfg_t* cfg) 63 | { 64 | int target_uid = 0; 65 | int target_gid = 0; 66 | const char* user = cfg_getstr(cfg, "unprivileged-user"); 67 | const char* chroot_dir = cfg_getstr(cfg, "chroot-dir"); 68 | if (NONEMPTY_STRING(user)) 69 | { 70 | #ifdef HAVE_PWD_H 71 | struct passwd* pwd = NULL; 72 | pwd = getpwnam(user); 73 | if (pwd == NULL) 74 | { 75 | log_error("cannot drop privileges: no such user: %s", user); 76 | return false; 77 | } 78 | target_uid = pwd->pw_uid; 79 | target_gid = pwd->pw_gid; 80 | if (chdir(pwd->pw_dir) < 0 && NULL_OR_EMPTY_STRING(chroot_dir)) 81 | { 82 | log_warn("cannot chdir to home directory of user %s: %s", user, 83 | strerror(errno)); 84 | } 85 | #else 86 | log_error("cannot drop privileges: not supported by system"); 87 | return false; 88 | #endif 89 | } 90 | if (NONEMPTY_STRING(chroot_dir)) 91 | { 92 | #ifdef HAVE_CHROOT 93 | if (chdir(chroot_dir) < 0) 94 | { 95 | log_perror(errno, 96 | "cannot drop privileges: failed to chdir to chroot"); 97 | return false; 98 | } 99 | if (chroot(chroot_dir) < 0) 100 | { 101 | log_perror(errno, "cannot drop privileges: chroot"); 102 | return false; 103 | } 104 | #else 105 | log_error("chroot is not supported on this system"); 106 | return false; 107 | #endif 108 | } 109 | if (target_uid != 0 || target_gid != 0) 110 | { 111 | #ifdef HAVE_SETGROUPS 112 | if (setgroups(0, NULL) < 0) 113 | { 114 | log_perror(errno, "cannot drop privileges: setgroups"); 115 | return false; 116 | } 117 | #endif 118 | if (setgid(target_gid) < 0) 119 | { 120 | log_perror(errno, "cannot drop privileges: setgid"); 121 | return false; 122 | } 123 | if (setuid(target_uid) < 0) 124 | { 125 | log_perror(errno, "cannot drop privileges: setuid"); 126 | return false; 127 | } 128 | } 129 | return true; 130 | } 131 | 132 | static bool prepare_database(cfg_t* cfg) 133 | { 134 | if (cfg_getint(cfg, "original-envelope") == SRS_ENVELOPE_DATABASE) 135 | { 136 | database_t* db = 137 | database_connect(cfg_getstr(cfg, "envelope-database"), true); 138 | if (db == NULL) 139 | return false; 140 | database_expire(db); 141 | database_disconnect(db); 142 | } 143 | return true; 144 | } 145 | 146 | static bool daemonize(cfg_t* cfg) 147 | { 148 | if (!cfg_getbool(cfg, "daemonize")) 149 | return true; 150 | close(0); 151 | close(1); 152 | close(2); 153 | if (fork() != 0) 154 | exit(EXIT_SUCCESS); 155 | setsid(); 156 | if (fork() != 0) 157 | exit(EXIT_SUCCESS); 158 | return true; 159 | } 160 | 161 | static void on_sigalrm(int signum) 162 | { 163 | timeout = signum; 164 | } 165 | 166 | static void handle_socketmap_client(cfg_t* cfg, srs_t* srs, 167 | const char* srs_domain, 168 | domain_set_t* local_domains, int conn) 169 | { 170 | #ifdef HAVE_FCNTL_H 171 | int flags = fcntl(conn, F_GETFL); 172 | if (flags & O_NONBLOCK) 173 | { 174 | if (fcntl(conn, F_SETFL, flags & ~O_NONBLOCK) < 0) 175 | { 176 | log_error("failed to make socket connection blocking"); 177 | return; 178 | } 179 | } 180 | #endif 181 | FILE* fp_write = fdopen(dup(conn), "w"); 182 | if (fp_write == NULL) 183 | return; 184 | FILE* fp_read = fdopen(conn, "r"); 185 | if (fp_read == NULL) 186 | return; 187 | database_t* db = NULL; 188 | if (cfg_getint(cfg, "original-envelope") == SRS_ENVELOPE_DATABASE) 189 | { 190 | db = database_connect(cfg_getstr(cfg, "envelope-database"), false); 191 | if (db == NULL) 192 | return; 193 | } 194 | signal(SIGALRM, on_sigalrm); 195 | int keep_alive = cfg_getint(cfg, "keep-alive"); 196 | for (;;) 197 | { 198 | char buffer[1024]; 199 | size_t len; 200 | char* addr; 201 | bool error; 202 | timeout = 0; 203 | alarm(keep_alive); 204 | char* request = netstring_read(fp_read, buffer, sizeof(buffer), &len); 205 | if (timeout) 206 | break; 207 | if (request == NULL) 208 | { 209 | if (!feof(fp_read) && !ferror(fp_read)) 210 | { 211 | netstring_write(fp_write, "PERM Invalid query.", 19); 212 | fflush(fp_write); 213 | log_error("invalid socketmap query, closing connection"); 214 | } 215 | break; 216 | } 217 | alarm(0); 218 | char* query_type = strtok_r(request, " ", &addr); 219 | if (query_type == NULL) 220 | { 221 | netstring_write(fp_write, "PERM Invalid query.", 19); 222 | fflush(fp_write); 223 | log_error("invalid socketmap query, closing connection"); 224 | break; 225 | } 226 | if (len > 512 + (size_t)(addr - request)) 227 | { 228 | netstring_write(fp_write, "PERM Too big.", 13); 229 | fflush(fp_write); 230 | log_warn("socketmap query is too big"); 231 | continue; 232 | } 233 | char* rewritten = NULL; 234 | const char* info = NULL; 235 | if (strcmp(query_type, "forward") == 0) 236 | { 237 | rewritten = postsrsd_forward(addr, srs_domain, srs, db, 238 | local_domains, &error, &info); 239 | } 240 | else if (strcmp(query_type, "reverse") == 0) 241 | { 242 | rewritten = postsrsd_reverse(addr, srs, db, &error, &info); 243 | } 244 | else 245 | { 246 | error = true; 247 | info = "Invalid map."; 248 | log_warn("invalid key in socketmap query"); 249 | } 250 | if (rewritten) 251 | { 252 | strcpy(buffer, "OK "); 253 | strncat(buffer, rewritten, sizeof(buffer) - 4); 254 | free(rewritten); 255 | netstring_write(fp_write, buffer, strlen(buffer)); 256 | } 257 | else 258 | { 259 | if (error) 260 | { 261 | strcpy(buffer, "PERM "); 262 | } 263 | else 264 | { 265 | strcpy(buffer, "NOTFOUND "); 266 | } 267 | if (info) 268 | strncat(buffer, info, sizeof(buffer) - 10); 269 | netstring_write(fp_write, buffer, strlen(buffer)); 270 | } 271 | fflush(fp_write); 272 | } 273 | database_disconnect(db); 274 | } 275 | 276 | int main(int argc, char** argv) 277 | { 278 | cfg_t* cfg = NULL; 279 | srs_t* srs = NULL; 280 | domain_set_t* local_domains = NULL; 281 | char* srs_domain = NULL; 282 | FILE* pf = NULL; 283 | int socketmaps[4] = {-1, -1, -1, -1}; 284 | int num_sockets = 0; 285 | int milter_pid = 0; 286 | int exit_code = EXIT_FAILURE; 287 | #ifdef HAVE_CLOSE_RANGE 288 | close_range(3, ~0U, 0); 289 | #else 290 | for (int fd = 3; fd < 1024; ++fd) 291 | close(fd); 292 | #endif 293 | cfg = config_from_commandline(argc, argv); 294 | if (cfg == NULL) 295 | goto shutdown; 296 | if (cfg_getbool(cfg, "syslog")) 297 | log_enable_syslog(); 298 | if (cfg_getbool(cfg, "debug")) 299 | log_set_verbosity(LogDebug); 300 | srs = srs_from_config(cfg); 301 | if (srs == NULL) 302 | goto shutdown; 303 | if (!srs_domains_from_config(cfg, &srs_domain, &local_domains)) 304 | goto shutdown; 305 | const char* socketmap_endpoint = cfg_getstr(cfg, "socketmap"); 306 | if (NONEMPTY_STRING(socketmap_endpoint)) 307 | { 308 | num_sockets = endpoint_create( 309 | socketmap_endpoint, sizeof(socketmaps) / sizeof(int), socketmaps); 310 | if (num_sockets < 0) 311 | goto shutdown; 312 | } 313 | const char* milter_endpoint = cfg_getstr(cfg, "milter"); 314 | if (NONEMPTY_STRING(milter_endpoint)) 315 | { 316 | if (!milter_create(milter_endpoint)) 317 | goto shutdown; 318 | } 319 | else 320 | { 321 | milter_endpoint = NULL; 322 | } 323 | const char* pid_file = cfg_getstr(cfg, "pid-file"); 324 | if (NONEMPTY_STRING(pid_file)) 325 | { 326 | pf = fopen(pid_file, "w"); 327 | if (pf == NULL) 328 | { 329 | log_error("cannot open %s for writing", pid_file); 330 | goto shutdown; 331 | } 332 | } 333 | if (!drop_privileges(cfg)) 334 | goto shutdown; 335 | if (!prepare_database(cfg)) 336 | goto shutdown; 337 | if (!daemonize(cfg)) 338 | goto shutdown; 339 | if (pf != NULL) 340 | { 341 | fprintf(pf, "%d", (int)getpid()); 342 | fclose(pf); 343 | pf = NULL; 344 | } 345 | signal(SIGALRM, SIG_IGN); 346 | exit_code = EXIT_SUCCESS; 347 | if (num_sockets > 0) 348 | { 349 | if (NONEMPTY_STRING(milter_endpoint)) 350 | { 351 | milter_pid = fork(); 352 | if (milter_pid == 0) 353 | { 354 | for (unsigned i = 0; i < sizeof(socketmaps) / sizeof(int); ++i) 355 | { 356 | if (socketmaps[i] >= 0) 357 | close(socketmaps[i]); 358 | socketmaps[i] = -1; 359 | } 360 | milter_main(cfg, srs, srs_domain, local_domains); 361 | goto shutdown; 362 | } 363 | } 364 | struct pollfd fds[sizeof(socketmaps) / sizeof(int)]; 365 | for (unsigned i = 0; i < (unsigned)num_sockets; ++i) 366 | { 367 | fds[i].fd = socketmaps[i]; 368 | fds[i].events = POLLIN; 369 | } 370 | for (;;) 371 | { 372 | if (poll(fds, num_sockets, 1000) < 0) 373 | { 374 | if (errno == EINTR) 375 | continue; 376 | log_perror(errno, "poll"); 377 | goto shutdown; 378 | } 379 | for (unsigned i = 0; i < (unsigned)num_sockets; ++i) 380 | { 381 | if (fds[i].revents) 382 | { 383 | int conn = accept(fds[i].fd, NULL, NULL); 384 | if (conn < 0) 385 | { 386 | log_perror(errno, "accept"); 387 | continue; 388 | } 389 | pid_t pid = fork(); 390 | if (pid == 0) 391 | { 392 | handle_socketmap_client(cfg, srs, srs_domain, 393 | local_domains, conn); 394 | exit(EXIT_SUCCESS); 395 | } 396 | if (pid < 0) 397 | { 398 | log_perror(errno, "fork"); 399 | } 400 | close(conn); 401 | } 402 | } 403 | waitpid(-1, NULL, WNOHANG); 404 | } 405 | } 406 | else if (NONEMPTY_STRING(milter_endpoint)) 407 | { 408 | milter_main(cfg, srs, srs_domain, local_domains); 409 | } 410 | shutdown: 411 | for (unsigned i = 0; i < sizeof(socketmaps) / sizeof(int); ++i) 412 | if (socketmaps[i] >= 0) 413 | close(socketmaps[i]); 414 | if (pf != NULL) 415 | fclose(pf); 416 | free(srs_domain); 417 | if (local_domains != NULL) 418 | domain_set_destroy(local_domains); 419 | if (srs != NULL) 420 | srs_free(srs); 421 | if (cfg != NULL) 422 | cfg_free(cfg); 423 | if (milter_pid > 0) 424 | { 425 | kill(milter_pid, SIGTERM); 426 | waitpid(milter_pid, NULL, 0); 427 | } 428 | return exit_code; 429 | } 430 | -------------------------------------------------------------------------------- /src/milter.c: -------------------------------------------------------------------------------- 1 | /* PostSRSd - Sender Rewriting Scheme daemon for Postfix 2 | * Copyright 2012-2023 Timo Röhling 3 | * SPDX-License-Identifier: GPL-3.0-only 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #include "milter.h" 18 | 19 | #include "database.h" 20 | #include "postsrsd_build_config.h" 21 | #include "srs.h" 22 | #include "util.h" 23 | 24 | #ifdef HAVE_ERRNO_H 25 | # include 26 | #endif 27 | #ifdef HAVE_FCNTL_H 28 | # include 29 | #endif 30 | #ifdef HAVE_SYS_STAT_H 31 | # include 32 | #endif 33 | 34 | #ifdef WITH_MILTER 35 | # include 36 | # ifdef HAVE_UNISTD_H 37 | # include 38 | # endif 39 | # include 40 | # include 41 | 42 | # ifndef HAVE_STRNCASECMP 43 | # ifdef HAVE__STRNICMP 44 | # define strncasecmp _strnicmp 45 | # endif 46 | # endif 47 | 48 | static char* milter_uri = NULL; 49 | static char* milter_path = NULL; 50 | static int milter_lock = -1; 51 | 52 | static cfg_t* g_cfg = NULL; 53 | static srs_t* g_srs = NULL; 54 | static domain_set_t* g_local_domains = NULL; 55 | static const char* g_srs_domain = NULL; 56 | 57 | struct privdata 58 | { 59 | char* envfrom; 60 | list_t* envrcpt; 61 | }; 62 | typedef struct privdata privdata_t; 63 | #endif 64 | 65 | #ifdef WITH_MILTER 66 | static void free_privdata(SMFICTX* ctx) 67 | { 68 | privdata_t* priv = smfi_getpriv(ctx); 69 | if (priv == NULL) 70 | return; 71 | free(priv->envfrom); 72 | list_destroy(priv->envrcpt, free); 73 | free(priv); 74 | smfi_setpriv(ctx, NULL); 75 | } 76 | 77 | static privdata_t* new_privdata(SMFICTX* ctx) 78 | { 79 | free_privdata(ctx); 80 | privdata_t* priv = malloc(sizeof(privdata_t)); 81 | if (priv == NULL) 82 | return NULL; 83 | priv->envfrom = NULL; 84 | priv->envrcpt = list_create(); 85 | if (priv->envrcpt == NULL) 86 | { 87 | free(priv); 88 | return NULL; 89 | } 90 | smfi_setpriv(ctx, priv); 91 | return priv; 92 | } 93 | 94 | static sfsistat on_envfrom(SMFICTX* ctx, char** argv) 95 | { 96 | privdata_t* priv = new_privdata(ctx); 97 | if (priv == NULL) 98 | return SMFIS_TEMPFAIL; 99 | priv->envfrom = strip_brackets(argv[0]); 100 | if (priv->envfrom == NULL) 101 | { 102 | free_privdata(ctx); 103 | return SMFIS_TEMPFAIL; 104 | } 105 | return SMFIS_CONTINUE; 106 | } 107 | 108 | static sfsistat on_envrcpt(SMFICTX* ctx, char** argv) 109 | { 110 | privdata_t* priv = smfi_getpriv(ctx); 111 | if (priv == NULL) 112 | return SMFIS_TEMPFAIL; 113 | char* rcpt = strip_brackets(argv[0]); 114 | if (rcpt == NULL) 115 | { 116 | free_privdata(ctx); 117 | return SMFIS_TEMPFAIL; 118 | } 119 | if (!list_append(priv->envrcpt, rcpt)) 120 | { 121 | free(rcpt); 122 | free_privdata(ctx); 123 | return SMFIS_TEMPFAIL; 124 | } 125 | return SMFIS_CONTINUE; 126 | } 127 | 128 | static sfsistat on_eom(SMFICTX* ctx) 129 | { 130 | sfsistat status = SMFIS_TEMPFAIL; 131 | database_t* db = NULL; 132 | privdata_t* priv = smfi_getpriv(ctx); 133 | if (priv == NULL) 134 | goto done; 135 | if (cfg_getint(g_cfg, "original-envelope") == SRS_ENVELOPE_DATABASE) 136 | { 137 | db = database_connect(cfg_getstr(g_cfg, "envelope-database"), false); 138 | if (db == NULL) 139 | goto done; 140 | } 141 | size_t rcpt_size = list_size(priv->envrcpt); 142 | bool error = false; 143 | for (size_t i = 0; i < rcpt_size; ++i) 144 | { 145 | char* rcpt = (char*)list_get(priv->envrcpt, i); 146 | char* rewritten = postsrsd_reverse(rcpt, g_srs, db, &error, NULL); 147 | if (error) 148 | goto done; 149 | if (rewritten) 150 | { 151 | char* bracketed_old_rcpt = add_brackets(rcpt); 152 | char* bracketed_new_rcpt = add_brackets(rewritten); 153 | free(rewritten); 154 | if (smfi_delrcpt(ctx, bracketed_old_rcpt) != MI_SUCCESS) 155 | { 156 | free(bracketed_old_rcpt); 157 | free(bracketed_new_rcpt); 158 | goto done; 159 | } 160 | if (smfi_addrcpt(ctx, bracketed_new_rcpt) 161 | != MI_SUCCESS) // TODO maybe add ESMTP arguments? 162 | { 163 | free(bracketed_old_rcpt); 164 | free(bracketed_new_rcpt); 165 | goto done; 166 | } 167 | free(bracketed_old_rcpt); 168 | free(bracketed_new_rcpt); 169 | } 170 | } 171 | if (*priv->envfrom) 172 | { 173 | // TODO check if mail is actually forwarded 174 | char* rewritten = postsrsd_forward(priv->envfrom, g_srs_domain, g_srs, 175 | db, g_local_domains, &error, NULL); 176 | if (error) 177 | goto done; 178 | if (rewritten) 179 | { 180 | char* bracketed_from = add_brackets(rewritten); 181 | free(rewritten); 182 | if (smfi_chgfrom(ctx, bracketed_from, 183 | NULL) 184 | != MI_SUCCESS) // TODO maybe add ESMTP arguments? 185 | { 186 | free(bracketed_from); 187 | goto done; 188 | } 189 | free(bracketed_from); 190 | } 191 | } 192 | status = SMFIS_CONTINUE; 193 | done: 194 | if (db) 195 | database_disconnect(db); 196 | free_privdata(ctx); 197 | return status; 198 | } 199 | 200 | static sfsistat on_abort(SMFICTX* ctx) 201 | { 202 | free_privdata(ctx); 203 | return SMFIS_CONTINUE; 204 | } 205 | 206 | /* clang-format off */ 207 | static struct smfiDesc smfilter = { 208 | "PostSRSd", SMFI_VERSION, SMFIF_CHGFROM | SMFIF_ADDRCPT | SMFIF_DELRCPT, 209 | NULL /* connect */, 210 | NULL /* helo */, 211 | on_envfrom, 212 | on_envrcpt, 213 | NULL /* header */, 214 | NULL /* eoh */, 215 | NULL /* body */, 216 | on_eom, 217 | on_abort, 218 | NULL /* close */, 219 | NULL /* unknown */, 220 | NULL /* data */, 221 | NULL /* negotiate */, 222 | }; 223 | /* clang-format on */ 224 | #endif 225 | 226 | bool milter_create(const char* uri) 227 | { 228 | #ifdef WITH_MILTER 229 | milter_uri = endpoint_for_milter(uri); 230 | if (milter_uri == NULL) 231 | { 232 | log_error("invalid milter endpoint: %s", uri); 233 | return false; 234 | } 235 | if (strncasecmp(milter_uri, "unix:", 5) == 0) 236 | milter_path = milter_uri + 5; 237 | else if (strncasecmp(milter_uri, "local:", 6) == 0) 238 | milter_path = milter_uri + 6; 239 | if (milter_path) 240 | milter_lock = acquire_lock(milter_path); 241 | if (milter_lock > 0) 242 | unlink(milter_path); 243 | if (smfi_setconn(milter_uri) == MI_FAILURE) 244 | { 245 | log_error("cannot start milter: smfi_setconn failed"); 246 | goto done; 247 | } 248 | if (smfi_register(smfilter) == MI_FAILURE) 249 | { 250 | log_error("cannot start milter: failed to register callbacks"); 251 | goto done; 252 | } 253 | if (smfi_opensocket(false) == MI_FAILURE) 254 | { 255 | log_error("cannot start milter: failed to open socket"); 256 | goto done; 257 | } 258 | if (milter_path) 259 | { 260 | if (chmod(milter_path, 0666) < 0) 261 | { 262 | log_perror(errno, "cannot start milter: cannot chmod() socket"); 263 | goto done; 264 | } 265 | } 266 | return true; 267 | done: 268 | if (milter_path != NULL && milter_lock > 0) 269 | { 270 | release_lock(milter_path, milter_lock); 271 | } 272 | milter_path = NULL; 273 | milter_lock = 0; 274 | free(milter_uri); 275 | return false; 276 | #else 277 | MAYBE_UNUSED(uri); 278 | log_error("no milter support"); 279 | return false; 280 | #endif 281 | } 282 | 283 | void milter_main(cfg_t* cfg, srs_t* srs, const char* srs_domain, 284 | domain_set_t* local_domains) 285 | { 286 | MAYBE_UNUSED(cfg); 287 | MAYBE_UNUSED(srs); 288 | MAYBE_UNUSED(srs_domain); 289 | MAYBE_UNUSED(local_domains); 290 | #ifdef WITH_MILTER 291 | g_cfg = cfg; 292 | g_srs = srs; 293 | g_srs_domain = srs_domain; 294 | g_local_domains = local_domains; 295 | smfi_main(); 296 | if (milter_path != NULL && milter_lock > 0) 297 | { 298 | release_lock(milter_path, milter_lock); 299 | } 300 | milter_path = NULL; 301 | milter_lock = 0; 302 | free(milter_uri); 303 | #endif 304 | } 305 | -------------------------------------------------------------------------------- /src/milter.h: -------------------------------------------------------------------------------- 1 | /* PostSRSd - Sender Rewriting Scheme daemon for Postfix 2 | * Copyright 2012-2022 Timo Röhling 3 | * SPDX-License-Identifier: GPL-3.0-only 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #ifndef MILTER_H 18 | #define MILTER_H 19 | 20 | #include "config.h" 21 | #include "srs2.h" 22 | #include "util.h" 23 | 24 | #include 25 | 26 | bool milter_create(const char* uri); 27 | void milter_main(cfg_t* cfg, srs_t* srs, const char* srs_domain, 28 | domain_set_t* local_domains); 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /src/netstring.c: -------------------------------------------------------------------------------- 1 | /* PostSRSd - Sender Rewriting Scheme daemon for Postfix 2 | * Copyright 2012-2022 Timo Röhling 3 | * SPDX-License-Identifier: GPL-3.0-only 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #include "netstring.h" 18 | 19 | #include 20 | 21 | char* netstring_encode(const char* data, size_t length, char* buffer, 22 | size_t bufsize, size_t* encoded_length) 23 | { 24 | if (data == NULL) 25 | return NULL; 26 | int i = snprintf(buffer, bufsize, "%zu:", length); 27 | if (i <= 0 || length >= bufsize - i) 28 | return NULL; 29 | strncpy(&buffer[i], data, length); 30 | buffer[length + i] = ','; 31 | if (encoded_length) 32 | *encoded_length = length + i + 1; 33 | return buffer; 34 | } 35 | 36 | char* netstring_decode(const char* netstring, char* buffer, size_t bufsize, 37 | size_t* decoded_length) 38 | { 39 | if (netstring == NULL) 40 | return NULL; 41 | int i = -1; 42 | size_t length; 43 | if (sscanf(netstring, "%5zu%n", &length, &i) < 1) 44 | return NULL; 45 | if (i < 0 || length >= bufsize) 46 | return NULL; 47 | if (netstring[i] != ':' || netstring[length + i + 1] != ',') 48 | return NULL; 49 | strncpy(buffer, &netstring[i + 1], length); 50 | if (decoded_length) 51 | *decoded_length = length; 52 | buffer[length] = 0; 53 | return buffer; 54 | } 55 | 56 | char* netstring_read(FILE* f, char* buffer, size_t bufsize, 57 | size_t* decoded_length) 58 | { 59 | size_t length; 60 | if (fscanf(f, "%5zu", &length) != 1) 61 | return NULL; 62 | if (fgetc(f) != ':') 63 | return NULL; 64 | if (length >= bufsize) 65 | return NULL; 66 | if (fread(buffer, 1, length, f) != length) 67 | return NULL; 68 | if (fgetc(f) != ',') 69 | return NULL; 70 | if (decoded_length) 71 | *decoded_length = length; 72 | buffer[length] = 0; 73 | return buffer; 74 | } 75 | 76 | int netstring_write(FILE* f, const char* data, size_t length) 77 | { 78 | int i = fprintf(f, "%zu:", length); 79 | if (i < 0) 80 | return -1; 81 | if (fwrite(data, 1, length, f) != length) 82 | return -1; 83 | if (fputc(',', f) != ',') 84 | return -1; 85 | return length + i + 1; 86 | } 87 | -------------------------------------------------------------------------------- /src/netstring.h: -------------------------------------------------------------------------------- 1 | /* PostSRSd - Sender Rewriting Scheme daemon for Postfix 2 | * Copyright 2012-2022 Timo Röhling 3 | * SPDX-License-Identifier: GPL-3.0-only 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #ifndef NETSTRING_H 18 | #define NETSTRING_H 19 | 20 | #include 21 | #include 22 | 23 | char* netstring_encode(const char* data, size_t length, char* buffer, 24 | size_t bufsize, size_t* encoded_length); 25 | char* netstring_decode(const char* netstring, char* buffer, size_t bufsize, 26 | size_t* decoded_length); 27 | char* netstring_read(FILE* f, char* buffer, size_t bufsize, 28 | size_t* decoded_length); 29 | int netstring_write(FILE* f, const char* data, size_t length); 30 | 31 | #endif 32 | -------------------------------------------------------------------------------- /src/postsrsd_build_config.h.in: -------------------------------------------------------------------------------- 1 | /* PostSRSd - Sender Rewriting Scheme daemon for Postfix 2 | * Copyright 2012-2023 Timo Röhling 3 | * SPDX-License-Identifier: GPL-3.0-only 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #ifndef POSTSRSD_BUILD_CONFIG_H 18 | #define POSTSRSD_BUILD_CONFIG_H 19 | 20 | /* clang-format off */ 21 | #define POSTSRSD_VERSION "@PROJECT_VERSION@" 22 | #define SIZEOF_UNSIGNED_LONG @SIZEOF_UNSIGNED_LONG@ 23 | #define DEFAULT_CONFIG_FILE "@POSTSRSD_CONFIGDIR@/@PROJECT_NAME@.conf" 24 | #define DEFAULT_SECRETS_FILE "@POSTSRSD_CONFIGDIR@/@PROJECT_NAME@.secret" 25 | #define DEFAULT_CHROOT_DIR "@POSTSRSD_CHROOTDIR@" 26 | #define DEFAULT_POSTSRSD_USER "@POSTSRSD_USER@" 27 | /* clang-format on */ 28 | 29 | #cmakedefine WITH_MILTER 1 30 | #cmakedefine WITH_REDIS 1 31 | #cmakedefine WITH_SQLITE 1 32 | 33 | #cmakedefine HAVE_BIG_ENDIAN 1 34 | #cmakedefine HAVE_CHROOT 1 35 | #cmakedefine HAVE_CLOSE_RANGE 1 36 | #cmakedefine HAVE_CLOSE_RANGE_GNU 1 37 | #cmakedefine HAVE_SETGROUPS 1 38 | #cmakedefine HAVE_STRCASECMP 1 39 | #cmakedefine HAVE__STRICMP 1 40 | #cmakedefine HAVE_STRNCASECMP 1 41 | #cmakedefine HAVE__STRNICMP 1 42 | 43 | #cmakedefine HAVE_ALLOCA_H 1 44 | #cmakedefine HAVE_ERRNO_H 1 45 | #cmakedefine HAVE_FCNTL_H 1 46 | #cmakedefine HAVE_GRP_H 1 47 | #cmakedefine HAVE_NETDB_H 1 48 | #cmakedefine HAVE_POLL_H 1 49 | #cmakedefine HAVE_PWD_H 1 50 | #cmakedefine HAVE_SIGNAL_H 1 51 | #cmakedefine HAVE_SYS_FILE_H 1 52 | #cmakedefine HAVE_SYS_INOTIFY_H 1 53 | #cmakedefine HAVE_SYS_SOCKET_H 1 54 | #cmakedefine HAVE_SYS_STAT_H 1 55 | #cmakedefine HAVE_SYS_TIME_H 1 56 | #cmakedefine HAVE_SYS_TYPES_H 1 57 | #cmakedefine HAVE_SYS_UN_H 1 58 | #cmakedefine HAVE_SYS_WAIT_H 1 59 | #cmakedefine HAVE_SYSLOG_H 1 60 | #cmakedefine HAVE_TIME_H 1 61 | #cmakedefine HAVE_UNISTD_H 1 62 | 63 | #endif 64 | -------------------------------------------------------------------------------- /src/sha1.c: -------------------------------------------------------------------------------- 1 | /* Copyright Gisle Ass, Peter C. Gutmann, Bruce Schneier 2 | * Copyright 2004 Shevek 3 | * Copyright 2012-2022 Timo Röhling 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * 6 | * This file has been copied from libsrs2. Original copyright follows: 7 | */ 8 | 9 | /* NIST Secure Hash Algorithm */ 10 | /* Borrowed from SHA1.xs by Gisle Ass */ 11 | /* heavily modified by Uwe Hollerbach */ 12 | /* from Peter C. Gutmann's implementation as found in */ 13 | /* Applied Cryptography by Bruce Schneier */ 14 | /* Further modifications to include the "UNRAVEL" stuff, below */ 15 | /* HMAC functions by Shevek for inclusion in 16 | * libsrs2, under GPL-2 or BSD license. Combine this lot in any way 17 | * you think will stand up in court. I hope my intent is clear. */ 18 | 19 | /* This code is in the public domain */ 20 | 21 | #include "sha1.h" 22 | 23 | #include "postsrsd_build_config.h" 24 | 25 | #include /* memcpy, strcpy, memset */ 26 | 27 | #ifdef SIZEOF_UNSIGNED_LONG 28 | # if SIZEOF_UNSIGNED_LONG < 4 29 | # error "SHA1 requires an unsigned long of at least 32 bits" 30 | # endif 31 | #endif 32 | 33 | #if SIZEOF_UNSIGNED_LONG == 4 34 | # ifdef HAVE_BIG_ENDIAN 35 | # define BYTEORDER 0x4321 36 | # else 37 | # define BYTEORDER 0x1234 38 | # endif 39 | #elif SIZEOF_UNSIGNED_LONG == 8 40 | # ifdef HAVE_BIG_ENDIAN 41 | # define BYTEORDER 0x87654321 42 | # else 43 | # define BYTEORDER 0x12345678 44 | # endif 45 | #else 46 | # error "SHA1 requires an unsigned long of either 4 or 8 bytes" 47 | #endif 48 | 49 | /* UNRAVEL should be fastest & biggest */ 50 | /* UNROLL_LOOPS should be just as big, but slightly slower */ 51 | /* both undefined should be smallest and slowest */ 52 | 53 | #define SHA_VERSION 1 54 | #define UNRAVEL 55 | /* #define UNROLL_LOOPS */ 56 | 57 | /* SHA f()-functions */ 58 | #define f1(x, y, z) ((x & y) | (~x & z)) 59 | #define f2(x, y, z) (x ^ y ^ z) 60 | #define f3(x, y, z) ((x & y) | (x & z) | (y & z)) 61 | #define f4(x, y, z) (x ^ y ^ z) 62 | 63 | /* SHA constants */ 64 | #define CONST1 0x5a827999L 65 | #define CONST2 0x6ed9eba1L 66 | #define CONST3 0x8f1bbcdcL 67 | #define CONST4 0xca62c1d6L 68 | 69 | /* truncate to 32 bits -- should be a null op on 32-bit machines */ 70 | #define T32(x) ((x) & 0xffffffffL) 71 | 72 | /* 32-bit rotate */ 73 | #define R32(x, n) T32(((x << n) | (x >> (32 - n)))) 74 | 75 | /* the generic case, for when the overall rotation is not unraveled */ 76 | #define FG(n) \ 77 | T = T32(R32(A, 5) + f##n(B, C, D) + E + *WP++ + CONST##n); \ 78 | E = D; \ 79 | D = C; \ 80 | C = R32(B, 30); \ 81 | B = A; \ 82 | A = T 83 | 84 | /* specific cases, for when the overall rotation is unraveled */ 85 | #define FA(n) \ 86 | T = T32(R32(A, 5) + f##n(B, C, D) + E + *WP++ + CONST##n); \ 87 | B = R32(B, 30) 88 | 89 | #define FB(n) \ 90 | E = T32(R32(T, 5) + f##n(A, B, C) + D + *WP++ + CONST##n); \ 91 | A = R32(A, 30) 92 | 93 | #define FC(n) \ 94 | D = T32(R32(E, 5) + f##n(T, A, B) + C + *WP++ + CONST##n); \ 95 | T = R32(T, 30) 96 | 97 | #define FD(n) \ 98 | C = T32(R32(D, 5) + f##n(E, T, A) + B + *WP++ + CONST##n); \ 99 | E = R32(E, 30) 100 | 101 | #define FE(n) \ 102 | B = T32(R32(C, 5) + f##n(D, E, T) + A + *WP++ + CONST##n); \ 103 | D = R32(D, 30) 104 | 105 | #define FT(n) \ 106 | A = T32(R32(B, 5) + f##n(C, D, E) + T + *WP++ + CONST##n); \ 107 | C = R32(C, 30) 108 | 109 | static void sha_transform(SHA_INFO* sha_info) 110 | { 111 | int i; 112 | sha_byte* dp; 113 | ULONG T, A, B, C, D, E, W[80], *WP; 114 | 115 | dp = sha_info->data; 116 | 117 | /* 118 | the following makes sure that at least one code block below is 119 | traversed or an error is reported, without the necessity for nested 120 | preprocessor if/else/endif blocks, which are a great pain in the 121 | nether regions of the anatomy... 122 | */ 123 | #undef SWAP_DONE 124 | 125 | #if BYTEORDER == 0x1234 126 | # define SWAP_DONE 127 | /* assert(sizeof(ULONG) == 4); */ 128 | for (i = 0; i < 16; ++i) 129 | { 130 | T = *((ULONG*)dp); 131 | dp += 4; 132 | W[i] = ((T << 24) & 0xff000000) | ((T << 8) & 0x00ff0000) 133 | | ((T >> 8) & 0x0000ff00) | ((T >> 24) & 0x000000ff); 134 | } 135 | #endif 136 | 137 | #if BYTEORDER == 0x4321 138 | # define SWAP_DONE 139 | /* assert(sizeof(ULONG) == 4); */ 140 | for (i = 0; i < 16; ++i) 141 | { 142 | T = *((ULONG*)dp); 143 | dp += 4; 144 | W[i] = T32(T); 145 | } 146 | #endif 147 | 148 | #if BYTEORDER == 0x12345678 149 | # define SWAP_DONE 150 | /* assert(sizeof(ULONG) == 8); */ 151 | for (i = 0; i < 16; i += 2) 152 | { 153 | T = *((ULONG*)dp); 154 | dp += 8; 155 | W[i] = ((T << 24) & 0xff000000) | ((T << 8) & 0x00ff0000) 156 | | ((T >> 8) & 0x0000ff00) | ((T >> 24) & 0x000000ff); 157 | T >>= 32; 158 | W[i + 1] = ((T << 24) & 0xff000000) | ((T << 8) & 0x00ff0000) 159 | | ((T >> 8) & 0x0000ff00) | ((T >> 24) & 0x000000ff); 160 | } 161 | #endif 162 | 163 | #if BYTEORDER == 0x87654321 164 | # define SWAP_DONE 165 | /* assert(sizeof(ULONG) == 8); */ 166 | for (i = 0; i < 16; i += 2) 167 | { 168 | T = *((ULONG*)dp); 169 | dp += 8; 170 | W[i] = T32(T >> 32); 171 | W[i + 1] = T32(T); 172 | } 173 | #endif 174 | 175 | #ifndef SWAP_DONE 176 | # error Unknown byte order -- you need to add code here 177 | #endif 178 | 179 | for (i = 16; i < 80; ++i) 180 | { 181 | W[i] = W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16]; 182 | #if (SHA_VERSION == 1) 183 | W[i] = R32(W[i], 1); 184 | #endif 185 | } 186 | A = sha_info->digest[0]; 187 | B = sha_info->digest[1]; 188 | C = sha_info->digest[2]; 189 | D = sha_info->digest[3]; 190 | E = sha_info->digest[4]; 191 | WP = W; 192 | #ifdef UNRAVEL 193 | /* clang-format off */ 194 | FA(1); FB(1); FC(1); FD(1); FE(1); FT(1); FA(1); FB(1); FC(1); FD(1); 195 | FE(1); FT(1); FA(1); FB(1); FC(1); FD(1); FE(1); FT(1); FA(1); FB(1); 196 | FC(2); FD(2); FE(2); FT(2); FA(2); FB(2); FC(2); FD(2); FE(2); FT(2); 197 | FA(2); FB(2); FC(2); FD(2); FE(2); FT(2); FA(2); FB(2); FC(2); FD(2); 198 | FE(3); FT(3); FA(3); FB(3); FC(3); FD(3); FE(3); FT(3); FA(3); FB(3); 199 | FC(3); FD(3); FE(3); FT(3); FA(3); FB(3); FC(3); FD(3); FE(3); FT(3); 200 | FA(4); FB(4); FC(4); FD(4); FE(4); FT(4); FA(4); FB(4); FC(4); FD(4); 201 | FE(4); FT(4); FA(4); FB(4); FC(4); FD(4); FE(4); FT(4); FA(4); FB(4); 202 | /* clang-format on */ 203 | sha_info->digest[0] = T32(sha_info->digest[0] + E); 204 | sha_info->digest[1] = T32(sha_info->digest[1] + T); 205 | sha_info->digest[2] = T32(sha_info->digest[2] + A); 206 | sha_info->digest[3] = T32(sha_info->digest[3] + B); 207 | sha_info->digest[4] = T32(sha_info->digest[4] + C); 208 | #else 209 | # ifdef UNROLL_LOOPS 210 | /* clang-format off */ 211 | FG(1); FG(1); FG(1); FG(1); FG(1); FG(1); FG(1); FG(1); FG(1); FG(1); 212 | FG(1); FG(1); FG(1); FG(1); FG(1); FG(1); FG(1); FG(1); FG(1); FG(1); 213 | FG(2); FG(2); FG(2); FG(2); FG(2); FG(2); FG(2); FG(2); FG(2); FG(2); 214 | FG(2); FG(2); FG(2); FG(2); FG(2); FG(2); FG(2); FG(2); FG(2); FG(2); 215 | FG(3); FG(3); FG(3); FG(3); FG(3); FG(3); FG(3); FG(3); FG(3); FG(3); 216 | FG(3); FG(3); FG(3); FG(3); FG(3); FG(3); FG(3); FG(3); FG(3); FG(3); 217 | FG(4); FG(4); FG(4); FG(4); FG(4); FG(4); FG(4); FG(4); FG(4); FG(4); 218 | FG(4); FG(4); FG(4); FG(4); FG(4); FG(4); FG(4); FG(4); FG(4); FG(4); 219 | /* clang-format on */ 220 | # else 221 | for (i = 0; i < 20; ++i) 222 | { 223 | FG(1); 224 | } 225 | for (i = 20; i < 40; ++i) 226 | { 227 | FG(2); 228 | } 229 | for (i = 40; i < 60; ++i) 230 | { 231 | FG(3); 232 | } 233 | for (i = 60; i < 80; ++i) 234 | { 235 | FG(4); 236 | } 237 | # endif 238 | sha_info->digest[0] = T32(sha_info->digest[0] + A); 239 | sha_info->digest[1] = T32(sha_info->digest[1] + B); 240 | sha_info->digest[2] = T32(sha_info->digest[2] + C); 241 | sha_info->digest[3] = T32(sha_info->digest[3] + D); 242 | sha_info->digest[4] = T32(sha_info->digest[4] + E); 243 | #endif 244 | } 245 | 246 | /* initialize the SHA digest */ 247 | 248 | static void sha_init(SHA_INFO* sha_info) 249 | { 250 | sha_info->digest[0] = 0x67452301L; 251 | sha_info->digest[1] = 0xefcdab89L; 252 | sha_info->digest[2] = 0x98badcfeL; 253 | sha_info->digest[3] = 0x10325476L; 254 | sha_info->digest[4] = 0xc3d2e1f0L; 255 | sha_info->count_lo = 0L; 256 | sha_info->count_hi = 0L; 257 | sha_info->local = 0; 258 | } 259 | 260 | /* update the SHA digest */ 261 | 262 | static void sha_update(SHA_INFO* sha_info, const sha_byte* buffer, int count) 263 | { 264 | int i; 265 | ULONG clo; 266 | 267 | clo = T32(sha_info->count_lo + ((ULONG)count << 3)); 268 | if (clo < sha_info->count_lo) 269 | { 270 | ++sha_info->count_hi; 271 | } 272 | sha_info->count_lo = clo; 273 | sha_info->count_hi += (ULONG)count >> 29; 274 | if (sha_info->local) 275 | { 276 | i = SHA_BLOCKSIZE - sha_info->local; 277 | if (i > count) 278 | { 279 | i = count; 280 | } 281 | memcpy(((sha_byte*)sha_info->data) + sha_info->local, buffer, i); 282 | count -= i; 283 | buffer += i; 284 | sha_info->local += i; 285 | if (sha_info->local == SHA_BLOCKSIZE) 286 | { 287 | sha_transform(sha_info); 288 | } 289 | else 290 | { 291 | return; 292 | } 293 | } 294 | while (count >= SHA_BLOCKSIZE) 295 | { 296 | memcpy(sha_info->data, buffer, SHA_BLOCKSIZE); 297 | buffer += SHA_BLOCKSIZE; 298 | count -= SHA_BLOCKSIZE; 299 | sha_transform(sha_info); 300 | } 301 | memcpy(sha_info->data, buffer, count); 302 | sha_info->local = count; 303 | } 304 | 305 | static void sha_transform_and_copy(unsigned char digest[20], SHA_INFO* sha_info) 306 | { 307 | sha_transform(sha_info); 308 | digest[0] = (unsigned char)((sha_info->digest[0] >> 24) & 0xff); 309 | digest[1] = (unsigned char)((sha_info->digest[0] >> 16) & 0xff); 310 | digest[2] = (unsigned char)((sha_info->digest[0] >> 8) & 0xff); 311 | digest[3] = (unsigned char)((sha_info->digest[0]) & 0xff); 312 | digest[4] = (unsigned char)((sha_info->digest[1] >> 24) & 0xff); 313 | digest[5] = (unsigned char)((sha_info->digest[1] >> 16) & 0xff); 314 | digest[6] = (unsigned char)((sha_info->digest[1] >> 8) & 0xff); 315 | digest[7] = (unsigned char)((sha_info->digest[1]) & 0xff); 316 | digest[8] = (unsigned char)((sha_info->digest[2] >> 24) & 0xff); 317 | digest[9] = (unsigned char)((sha_info->digest[2] >> 16) & 0xff); 318 | digest[10] = (unsigned char)((sha_info->digest[2] >> 8) & 0xff); 319 | digest[11] = (unsigned char)((sha_info->digest[2]) & 0xff); 320 | digest[12] = (unsigned char)((sha_info->digest[3] >> 24) & 0xff); 321 | digest[13] = (unsigned char)((sha_info->digest[3] >> 16) & 0xff); 322 | digest[14] = (unsigned char)((sha_info->digest[3] >> 8) & 0xff); 323 | digest[15] = (unsigned char)((sha_info->digest[3]) & 0xff); 324 | digest[16] = (unsigned char)((sha_info->digest[4] >> 24) & 0xff); 325 | digest[17] = (unsigned char)((sha_info->digest[4] >> 16) & 0xff); 326 | digest[18] = (unsigned char)((sha_info->digest[4] >> 8) & 0xff); 327 | digest[19] = (unsigned char)((sha_info->digest[4]) & 0xff); 328 | } 329 | 330 | /* finish computing the SHA digest */ 331 | static void sha_final(unsigned char digest[20], SHA_INFO* sha_info) 332 | { 333 | int count; 334 | ULONG lo_bit_count, hi_bit_count; 335 | 336 | lo_bit_count = sha_info->count_lo; 337 | hi_bit_count = sha_info->count_hi; 338 | count = (int)((lo_bit_count >> 3) & 0x3f); 339 | ((sha_byte*)sha_info->data)[count++] = 0x80; 340 | if (count > SHA_BLOCKSIZE - 8) 341 | { 342 | memset(((sha_byte*)sha_info->data) + count, 0, SHA_BLOCKSIZE - count); 343 | sha_transform(sha_info); 344 | memset((sha_byte*)sha_info->data, 0, SHA_BLOCKSIZE - 8); 345 | } 346 | else 347 | { 348 | memset(((sha_byte*)sha_info->data) + count, 0, 349 | SHA_BLOCKSIZE - 8 - count); 350 | } 351 | sha_info->data[56] = (hi_bit_count >> 24) & 0xff; 352 | sha_info->data[57] = (hi_bit_count >> 16) & 0xff; 353 | sha_info->data[58] = (hi_bit_count >> 8) & 0xff; 354 | sha_info->data[59] = (hi_bit_count >> 0) & 0xff; 355 | sha_info->data[60] = (lo_bit_count >> 24) & 0xff; 356 | sha_info->data[61] = (lo_bit_count >> 16) & 0xff; 357 | sha_info->data[62] = (lo_bit_count >> 8) & 0xff; 358 | sha_info->data[63] = (lo_bit_count >> 0) & 0xff; 359 | sha_transform_and_copy(digest, sha_info); 360 | } 361 | 362 | /********************************************************************/ 363 | /* 364 | SHA_INFO ctx; 365 | unsigned char *data; 366 | STRLEN len; 367 | unsigned char digeststr[20]; 368 | 369 | sha_init(&ctx); 370 | 371 | for (i = 0; i < items; i++) { 372 | data = (unsigned char *)(SvPVbyte(ST(i), len)); 373 | sha_update(&ctx, data, len); 374 | } 375 | sha_final(digeststr, &ctx); 376 | */ 377 | 378 | void sha_digest(char* out, const char* data, unsigned len) 379 | { 380 | SHA_INFO ctx; 381 | sha_init(&ctx); 382 | sha_update(&ctx, (const sha_byte*)data, len); 383 | sha_final((sha_byte*)out, &ctx); 384 | } 385 | 386 | void srs_hmac_init(srs_hmac_ctx_t* ctx, char* secret, unsigned len) 387 | { 388 | char sbuf[SHA_BLOCKSIZE]; 389 | unsigned i; 390 | 391 | if (len > SHA_BLOCKSIZE) 392 | { 393 | sha_digest(sbuf, secret, len); 394 | secret = sbuf; 395 | len = SHA_DIGESTSIZE; 396 | } 397 | 398 | memset(ctx->ipad, 0x36, SHA_BLOCKSIZE); 399 | memset(ctx->opad, 0x5c, SHA_BLOCKSIZE); 400 | for (i = 0; i < len; i++) 401 | { 402 | ctx->ipad[i] ^= secret[i]; 403 | ctx->opad[i] ^= secret[i]; 404 | } 405 | 406 | memset(sbuf, 0, SHA_BLOCKSIZE); 407 | 408 | sha_init(&ctx->sctx); 409 | sha_update(&ctx->sctx, (sha_byte*)ctx->ipad, SHA_BLOCKSIZE); 410 | } 411 | 412 | void srs_hmac_update(srs_hmac_ctx_t* ctx, char* data, unsigned len) 413 | { 414 | sha_update(&ctx->sctx, (sha_byte*)data, len); 415 | } 416 | 417 | void srs_hmac_fini(srs_hmac_ctx_t* ctx, char* out) 418 | { 419 | sha_byte buf[SHA_DIGESTSIZE + 1]; 420 | 421 | sha_final(buf, &ctx->sctx); 422 | sha_init(&ctx->sctx); 423 | sha_update(&ctx->sctx, (sha_byte*)ctx->opad, SHA_BLOCKSIZE); 424 | sha_update(&ctx->sctx, buf, SHA_DIGESTSIZE); 425 | sha_final((sha_byte*)out, &ctx->sctx); 426 | } 427 | -------------------------------------------------------------------------------- /src/sha1.h: -------------------------------------------------------------------------------- 1 | /* PostSRSd - Sender Rewriting Scheme daemon for Postfix 2 | * Copyright Gisle Ass, Peter C. Gutmann, Bruce Schneier 3 | * Copyright 2004 Shevek 4 | * Copyright 2012-2022 Timo Röhling 5 | * SPDX-License-Identifier: BSD-3-Clause 6 | */ 7 | #ifndef SHA1_H 8 | #define SHA1_H 9 | 10 | typedef unsigned long ULONG; /* 32-or-more-bit quantity */ 11 | typedef unsigned char sha_byte; 12 | 13 | #define SHA_BLOCKSIZE 64 14 | #define SHA_DIGESTSIZE 20 15 | 16 | typedef struct 17 | { 18 | ULONG digest[5]; /* message digest */ 19 | ULONG count_lo, count_hi; /* 64-bit bit count */ 20 | sha_byte data[SHA_BLOCKSIZE]; /* SHA data buffer */ 21 | int local; /* unprocessed amount in data */ 22 | } SHA_INFO; 23 | 24 | typedef struct _srs_hmac_ctx_t 25 | { 26 | SHA_INFO sctx; 27 | char ipad[SHA_BLOCKSIZE + 1]; 28 | char opad[SHA_BLOCKSIZE + 1]; 29 | } srs_hmac_ctx_t; 30 | 31 | void sha_digest(char* out, const char* data, unsigned len); 32 | void srs_hmac_init(srs_hmac_ctx_t* ctx, char* secret, unsigned len); 33 | void srs_hmac_update(srs_hmac_ctx_t* ctx, char* data, unsigned len); 34 | void srs_hmac_fini(srs_hmac_ctx_t* ctx, char* out); 35 | 36 | #endif 37 | -------------------------------------------------------------------------------- /src/srs.c: -------------------------------------------------------------------------------- 1 | /* PostSRSd - Sender Rewriting Scheme daemon for Postfix 2 | * Copyright 2012-2023 Timo Röhling 3 | * SPDX-License-Identifier: GPL-3.0-only 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #include "srs.h" 18 | 19 | #include "sha1.h" 20 | #include "util.h" 21 | 22 | #include 23 | #include 24 | 25 | char* postsrsd_forward(const char* addr, const char* domain, srs_t* srs, 26 | database_t* db, domain_set_t* local_domains, bool* error, 27 | const char** info) 28 | { 29 | const char* at = strchr(addr, '@'); 30 | if (error != NULL) 31 | *error = false; 32 | if (info != NULL) 33 | *info = NULL; 34 | if (at == NULL) 35 | { 36 | if (info != NULL) 37 | *info = "No domain."; 38 | log_debug("<%s> not rewritten: no domain", addr); 39 | return NULL; 40 | } 41 | const char* input_domain = at + 1; 42 | if (domain_set_contains(local_domains, input_domain)) 43 | { 44 | if (info != NULL) 45 | *info = "Need not rewrite local domain."; 46 | log_debug("<%s> not rewritten: local domain", addr); 47 | return NULL; 48 | } 49 | char db_alias_buf[35]; 50 | char* db_alias; 51 | const char* sender = addr; 52 | if (db != NULL && !SRS_IS_SRS_ADDRESS(addr)) 53 | { 54 | char digest[20]; 55 | sha_digest(digest, addr, strlen(addr)); 56 | db_alias = b32h_encode(digest, 20, db_alias_buf, sizeof(db_alias_buf)); 57 | if (db_alias == NULL) 58 | { 59 | log_warn("<%s> not rewritten: aliasing error", addr); 60 | if (error) 61 | *error = true; 62 | if (info) 63 | *info = "Aliasing error."; 64 | return NULL; 65 | } 66 | strcat(db_alias, "@1"); 67 | if (!database_write(db, db_alias, addr, srs->maxage * 86400)) 68 | { 69 | log_warn("<%s> not rewritten: database error", addr); 70 | if (error != NULL) 71 | *error = true; 72 | if (info != NULL) 73 | *info = "Database error."; 74 | return NULL; 75 | } 76 | sender = db_alias; 77 | } 78 | char* output = NULL; 79 | int result = srs_forward_alloc(srs, &output, sender, domain); 80 | if (result == SRS_SUCCESS) 81 | { 82 | log_info("<%s> forwarded as <%s>", addr, output); 83 | return output; 84 | } 85 | free(output); 86 | if (info != NULL) 87 | *info = srs_strerror(result); 88 | log_info("<%s> not rewritten: %s", addr, srs_strerror(result)); 89 | return NULL; 90 | } 91 | 92 | char* postsrsd_reverse(const char* addr, srs_t* srs, database_t* db, 93 | bool* error, const char** info) 94 | { 95 | char buffer[513]; 96 | if (error != NULL) 97 | *error = false; 98 | if (info != NULL) 99 | *info = NULL; 100 | int result = srs_reverse(srs, buffer, sizeof(buffer), addr); 101 | if (result != SRS_SUCCESS) 102 | { 103 | if (info != NULL) 104 | *info = srs_strerror(result); 105 | if (result != SRS_ENOTSRSADDRESS) 106 | { 107 | log_info("<%s> not reversed: %s", addr, srs_strerror(result)); 108 | } 109 | else 110 | { 111 | log_debug("<%s> not reversed: %s", addr, srs_strerror(result)); 112 | } 113 | return NULL; 114 | } 115 | const char* at = strchr(buffer, '@'); 116 | if (at == NULL) 117 | { 118 | log_info("<%s> not reversed: internal error", addr); 119 | if (error != NULL) 120 | *error = true; 121 | if (info != NULL) 122 | *info = "Internal error."; 123 | return NULL; 124 | } 125 | if (strcmp(at, "@1") == 0) 126 | { 127 | if (db != NULL) 128 | { 129 | char* p = buffer; 130 | while (*p) 131 | { 132 | *p = toupper(*p); 133 | ++p; 134 | } 135 | char* sender = database_read(db, buffer); 136 | if (sender == NULL) 137 | { 138 | log_info("<%s> not reversed: unknown alias", addr); 139 | if (info != NULL) 140 | *info = "Unknown alias."; 141 | return NULL; 142 | } 143 | log_info("<%s> reversed to <%s>", addr, sender); 144 | return sender; 145 | } 146 | else 147 | { 148 | log_warn("<%s> not reversed: no database for alias", addr); 149 | if (error) 150 | *error = true; 151 | if (info) 152 | *info = "No database for alias."; 153 | return NULL; 154 | } 155 | } 156 | log_info("<%s> reversed to <%s>", addr, buffer); 157 | return strdup(buffer); 158 | } 159 | -------------------------------------------------------------------------------- /src/srs.h: -------------------------------------------------------------------------------- 1 | /* PostSRSd - Sender Rewriting Scheme daemon for Postfix 2 | * Copyright 2012-2022 Timo Röhling 3 | * SPDX-License-Identifier: GPL-3.0-only 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #ifndef SRS_H 18 | #define SRS_H 19 | 20 | #include "database.h" 21 | #include "srs2.h" 22 | #include "util.h" 23 | 24 | #include 25 | 26 | char* postsrsd_forward(const char* addr, const char* domain, srs_t* srs, 27 | database_t* db, domain_set_t* local_domains, bool* error, 28 | const char** info); 29 | char* postsrsd_reverse(const char* addr, srs_t* srs, database_t* db, 30 | bool* error, const char** info); 31 | 32 | #endif 33 | -------------------------------------------------------------------------------- /src/srs2.h: -------------------------------------------------------------------------------- 1 | /* Copyright 2004 Shevek 2 | * Copyright 2012-2023 Timo Röhling 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | * 5 | * This file has been copied from libsrs2. Original copyright follows: 6 | */ 7 | 8 | /* Copyright (c) 2004 Shevek (srs@anarres.org) 9 | * All rights reserved. 10 | * 11 | * This file is a part of libsrs2 from http://www.libsrs2.org/ 12 | * 13 | * Redistribution and use in source and binary forms, with or without 14 | * modification, under the terms of either the GNU General Public 15 | * License version 2 or the BSD license, at the discretion of the 16 | * user. Copies of these licenses have been included in the libsrs2 17 | * distribution. See the the file called LICENSE for more 18 | * information. 19 | */ 20 | 21 | #ifndef __SRS2_H__ 22 | #define __SRS2_H__ 23 | 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #ifdef HAVE_SYS_TYPES_H 31 | # include 32 | #endif 33 | #ifdef HAVE_SYS_TIME_H 34 | # include 35 | #endif 36 | #ifdef HAVE_TIME_H 37 | # include 38 | #endif 39 | 40 | #ifndef __BEGIN_DECLS 41 | # define __BEGIN_DECLS 42 | # define __END_DECLS 43 | #endif 44 | 45 | __BEGIN_DECLS 46 | 47 | #define SRS_VERSION_MAJOR 1 48 | #define SRS_VERSION_MINOR 0 49 | #define SRS_VERSION_PATCHLEVEL 14 50 | #define SRS_VERSION_FROM(m, n, p) (((m) << 16) + ((n) << 8) + (p)) 51 | #define SRS_VERSION \ 52 | SRS_VERSION_FROM(SRS_VERSION_MAJOR, SRS_VERSION_MINOR, \ 53 | SRS_VERSION_PATCHLEVEL) 54 | 55 | /* This is ugly, but reasonably safe. */ 56 | #undef TRUE 57 | #define TRUE 1 58 | #undef FALSE 59 | #define FALSE 0 60 | 61 | #define SRSSEP '=' 62 | #define SRS0TAG "SRS0" 63 | #define SRS1TAG "SRS1" 64 | 65 | /* Error codes */ 66 | 67 | #define SRS_ERRTYPE_MASK 0xF000 68 | #define SRS_ERRTYPE_NONE 0x0000 69 | #define SRS_ERRTYPE_CONFIG 0x1000 70 | #define SRS_ERRTYPE_INPUT 0x2000 71 | #define SRS_ERRTYPE_SYNTAX 0x4000 72 | #define SRS_ERRTYPE_SRS 0x8000 73 | 74 | #define SRS_SUCCESS (0) 75 | #define SRS_ENOTSRSADDRESS (1) 76 | #define SRS_ENOTREWRITTEN (2) 77 | 78 | #define SRS_ENOSECRETS (SRS_ERRTYPE_CONFIG | 1) 79 | #define SRS_ESEPARATORINVALID (SRS_ERRTYPE_CONFIG | 2) 80 | 81 | #define SRS_ENOSENDERATSIGN (SRS_ERRTYPE_INPUT | 1) 82 | #define SRS_EBUFTOOSMALL (SRS_ERRTYPE_INPUT | 2) 83 | 84 | #define SRS_ENOSRS0HOST (SRS_ERRTYPE_SYNTAX | 1) 85 | #define SRS_ENOSRS0USER (SRS_ERRTYPE_SYNTAX | 2) 86 | #define SRS_ENOSRS0HASH (SRS_ERRTYPE_SYNTAX | 3) 87 | #define SRS_ENOSRS0STAMP (SRS_ERRTYPE_SYNTAX | 4) 88 | #define SRS_ENOSRS1HOST (SRS_ERRTYPE_SYNTAX | 5) 89 | #define SRS_ENOSRS1USER (SRS_ERRTYPE_SYNTAX | 6) 90 | #define SRS_ENOSRS1HASH (SRS_ERRTYPE_SYNTAX | 7) 91 | #define SRS_EBADTIMESTAMPCHAR (SRS_ERRTYPE_SYNTAX | 8) 92 | #define SRS_EHASHTOOSHORT (SRS_ERRTYPE_SYNTAX | 9) 93 | 94 | #define SRS_ETIMESTAMPOUTOFDATE (SRS_ERRTYPE_SRS | 1) 95 | #define SRS_EHASHINVALID (SRS_ERRTYPE_SRS | 2) 96 | 97 | #define SRS_ERROR_TYPE(x) ((x) & SRS_ERRTYPE_MASK) 98 | 99 | /* SRS implementation */ 100 | 101 | #define SRS_IS_SRS_ADDRESS(x) \ 102 | ((strncasecmp((x), "SRS", 3) == 0) && (strchr("01", (x)[3]) != NULL) \ 103 | && (strchr("-+=", (x)[4]) != NULL)) 104 | 105 | typedef void* (*srs_malloc_t)(size_t); 106 | typedef void* (*srs_realloc_t)(void*, size_t); 107 | typedef void (*srs_free_t)(void*); 108 | 109 | typedef int srs_bool; 110 | 111 | typedef struct _srs_t 112 | { 113 | /* Rewriting parameters */ 114 | char** secrets; 115 | int numsecrets; 116 | char separator; 117 | 118 | /* Security parameters */ 119 | int maxage; /* Maximum allowed age in seconds */ 120 | int hashlength; 121 | int hashmin; 122 | 123 | /* Behaviour parameters */ 124 | srs_bool alwaysrewrite; /* Rewrite even into same domain? */ 125 | srs_bool noforward; /* Never perform forwards rewriting */ 126 | srs_bool noreverse; /* Never perform reverse rewriting */ 127 | char** neverrewrite; /* A list of non-rewritten domains */ 128 | 129 | time_t faketime; /* Added for testing purposes */ 130 | } srs_t; 131 | 132 | /* Interface */ 133 | int srs_set_malloc(srs_malloc_t m, srs_realloc_t r, srs_free_t f); 134 | srs_t* srs_new(); 135 | void srs_init(srs_t* srs); 136 | void srs_free(srs_t* srs); 137 | int srs_forward(srs_t* srs, char* buf, unsigned buflen, const char* sender, 138 | const char* alias); 139 | int srs_forward_alloc(srs_t* srs, char** sptr, const char* sender, 140 | const char* alias); 141 | int srs_reverse(srs_t* srs, char* buf, unsigned buflen, const char* sender); 142 | int srs_reverse_alloc(srs_t* srs, char** sptr, const char* sender); 143 | const char* srs_strerror(int code); 144 | int srs_add_secret(srs_t* srs, const char* secret); 145 | const char* srs_get_secret(srs_t* srs, int idx); 146 | /* You probably shouldn't call these. */ 147 | int srs_timestamp_create(srs_t* srs, char* buf, time_t now); 148 | int srs_timestamp_check(srs_t* srs, const char* stamp); 149 | 150 | #define SRS_PARAM_DECLARE(n, t) \ 151 | int srs_set_##n(srs_t* srs, t value); \ 152 | t srs_get_##n(srs_t* srs); 153 | 154 | SRS_PARAM_DECLARE(alwaysrewrite, srs_bool) 155 | SRS_PARAM_DECLARE(separator, char) 156 | SRS_PARAM_DECLARE(maxage, int) 157 | SRS_PARAM_DECLARE(hashlength, int) 158 | SRS_PARAM_DECLARE(hashmin, int) 159 | SRS_PARAM_DECLARE(noforward, srs_bool) 160 | SRS_PARAM_DECLARE(noreverse, srs_bool) 161 | 162 | __END_DECLS 163 | 164 | #endif 165 | -------------------------------------------------------------------------------- /src/util.h: -------------------------------------------------------------------------------- 1 | /* PostSRSd - Sender Rewriting Scheme daemon for Postfix 2 | * Copyright 2012-2023 Timo Röhling 3 | * SPDX-License-Identifier: GPL-3.0-only 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #ifndef UTIL_H 18 | #define UTIL_H 19 | 20 | #include 21 | #include 22 | 23 | #define MAYBE_UNUSED(x) (void)(x) 24 | 25 | #ifdef __GNUC__ 26 | # define ATTRIBUTE(x) __attribute__((x)) 27 | #else 28 | # define ATTRIBUTE(x) 29 | #endif 30 | 31 | #define NONEMPTY_STRING(s) ((s) != NULL && *(s) != 0) 32 | #define NULL_OR_EMPTY_STRING(s) ((s) == NULL || *(s) == 0) 33 | 34 | struct domain_set; 35 | typedef struct domain_set domain_set_t; 36 | struct list; 37 | typedef struct list list_t; 38 | typedef void (*list_deleter_t)(void*); 39 | 40 | void set_string(char** var, char* value); 41 | char* b32h_encode(const char* data, size_t length, char* buffer, 42 | size_t bufsize); 43 | 44 | char** argvdup(char** argv); 45 | void freeargv(char** argv); 46 | 47 | char* strip_brackets(const char* addr); 48 | char* add_brackets(const char* addr); 49 | 50 | bool file_exists(const char* filename); 51 | bool directory_exists(const char* dirname); 52 | 53 | int acquire_lock(const char* path); 54 | void release_lock(const char* path, int fd); 55 | 56 | domain_set_t* domain_set_create(); 57 | bool domain_set_add(domain_set_t* D, const char* domain); 58 | bool domain_set_contains(domain_set_t* D, const char* domain); 59 | void domain_set_destroy(domain_set_t* D); 60 | 61 | list_t* list_create(); 62 | void* list_get(list_t* L, size_t i); 63 | bool list_append(list_t* L, void* data); 64 | size_t list_size(list_t* L); 65 | void list_clear(list_t* L, list_deleter_t deleter); 66 | void list_destroy(list_t* L, list_deleter_t deleter); 67 | 68 | char* endpoint_for_milter(const char* s); 69 | char* endpoint_for_redis(const char* s, int* port); 70 | 71 | enum log_priority 72 | { 73 | LogDebug, 74 | LogInfo, 75 | LogWarn, 76 | LogError, 77 | }; 78 | 79 | void log_enable_syslog(); 80 | void log_set_verbosity(enum log_priority prio); 81 | void log_debug(const char* fmt, ...) ATTRIBUTE(format(printf, 1, 2)); 82 | void log_info(const char* fmt, ...) ATTRIBUTE(format(printf, 1, 2)); 83 | void log_warn(const char* fmt, ...) ATTRIBUTE(format(printf, 1, 2)); 84 | void log_error(const char* fmt, ...) ATTRIBUTE(format(printf, 1, 2)); 85 | void log_perror(int errno, const char* prefix); 86 | void log_fatal(const char* fmt, ...) ATTRIBUTE(noreturn); 87 | 88 | #endif 89 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # PostSRSd - Sender Rewriting Scheme daemon for Postfix 2 | # Copyright 2012-2022 Timo Röhling 3 | # SPDX-License-Identifier: GPL-3.0-only 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, version 3. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | add_subdirectory(unit) 18 | add_subdirectory(blackbox) 19 | -------------------------------------------------------------------------------- /tests/blackbox/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # PostSRSd - Sender Rewriting Scheme daemon for Postfix 2 | # Copyright 2012-2023 Timo Röhling 3 | # SPDX-License-Identifier: GPL-3.0-only 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, version 3. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | find_package(Python3 3.3 QUIET) 18 | 19 | if(Python3_EXECUTABLE) 20 | add_test( 21 | NAME test_socketmap 22 | COMMAND 23 | "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/socketmap.py" 24 | "$" "$" 25 | ) 26 | if(WITH_MILTER) 27 | add_test( 28 | NAME test_milter 29 | COMMAND 30 | "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/milter.py" 31 | "$" 32 | ) 33 | endif() 34 | endif() 35 | -------------------------------------------------------------------------------- /tests/blackbox/milter.py: -------------------------------------------------------------------------------- 1 | # PostSRSd - Sender Rewriting Scheme daemon for Postfix 2 | # Copyright 2012-2023 Timo Röhling 3 | # SPDX-License-Identifier: GPL-3.0-only 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, version 3. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | import contextlib 18 | import os 19 | import pathlib 20 | import socket 21 | import signal 22 | import subprocess 23 | import struct 24 | import sys 25 | import tempfile 26 | import time 27 | 28 | 29 | def send_milter(sock, code, data): 30 | sock.send(struct.pack(">Lc", len(data) + 1, code) + data) 31 | 32 | 33 | def recv_milter(sock): 34 | size = struct.unpack(">L", sock.recv(4)) 35 | data = sock.recv(*size) 36 | return data[:1], data[1:] 37 | 38 | 39 | def mf_optneg(sock): 40 | send_milter(sock, b"O", struct.pack(">LLL", 6, 0xFF, 0xFF)) 41 | code, _ = recv_milter(sock) 42 | return code == b"O" 43 | 44 | 45 | def mf_connect(sock): 46 | send_milter(sock, b"C", b"mail.example.com\x00U") 47 | code, _ = recv_milter(sock) 48 | return code == b"c" 49 | 50 | 51 | def mf_envfrom(sock, envfrom): 52 | send_milter(sock, b"M", b"<" + envfrom.encode() + b">\x00") 53 | code, _ = recv_milter(sock) 54 | return code == b"c" 55 | 56 | 57 | def mf_rcptto(sock, rcptto): 58 | send_milter(sock, b"R", b"<" + rcptto.encode() + b">\x00") 59 | code, _ = recv_milter(sock) 60 | return code == b"c" 61 | 62 | 63 | def mf_eom(sock): 64 | new_from = None 65 | new_rcpt = None 66 | send_milter(sock, b"E", b"") 67 | code, data = recv_milter(sock) 68 | while code in [b"+", b"-", b"e"]: 69 | if code == b"+": 70 | new_rcpt = data[1:-2].decode() 71 | if code == b"e": 72 | new_from = data[1:-2].decode() 73 | code, data = recv_milter(sock) 74 | return code == b"c", new_from, new_rcpt 75 | 76 | 77 | @contextlib.contextmanager 78 | def postsrsd_instance(postsrsd, when): 79 | with tempfile.TemporaryDirectory() as tmpdirname: 80 | tmpdir = pathlib.Path(tmpdirname) 81 | with open(tmpdir / "postsrsd.conf", "w") as f: 82 | f.write( 83 | 'domains = {"example.com"}\n' 84 | "keep-alive = 2\n" 85 | 'chroot-dir = ""\n' 86 | 'unprivileged-user = ""\n' 87 | f"original-envelope = embedded\n" 88 | f'socketmap = ""\n' 89 | f'milter = unix:{tmpdir / "postsrsd.sock"}\n' 90 | f'secrets-file = {tmpdir / "postsrsd.secret"}\n' 91 | f'envelope-database = sqlite:{tmpdir / "postsrsd.db"}\n' 92 | ) 93 | with open(tmpdir / "postsrsd.secret", "w") as f: 94 | f.write("tops3cr3t\n") 95 | os.environ["POSTSRSD_FAKETIME"] = when 96 | proc = subprocess.Popen( 97 | [postsrsd, "-C", str(tmpdir / "postsrsd.conf")], 98 | start_new_session=True, 99 | ) 100 | wait = 50 101 | while not (tmpdir / "postsrsd.sock").exists() and wait > 0: 102 | time.sleep(0.1) 103 | wait -= 1 104 | try: 105 | yield str(tmpdir / "postsrsd.sock").encode() 106 | finally: 107 | os.killpg(os.getpgid(proc.pid), signal.SIGTERM) 108 | proc.wait() 109 | 110 | 111 | def execute_queries(postsrsd, when, queries): 112 | with postsrsd_instance(postsrsd, when) as endpoint: 113 | for query in queries: 114 | orig_from, orig_rcpt = query[0] 115 | new_from, new_rcpt = query[1] 116 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM, 0) 117 | try: 118 | sock.settimeout(0.5) 119 | sock.connect(endpoint) 120 | assert mf_optneg(sock) 121 | assert mf_connect(sock) 122 | assert mf_envfrom(sock, orig_from) 123 | assert mf_rcptto(sock, orig_rcpt) 124 | ok, srs_from, srs_rcpt = mf_eom(sock) 125 | assert ok, "mf_eom failed" 126 | assert srs_from == new_from 127 | assert srs_rcpt == new_rcpt 128 | sys.stderr.write(f"query[{query[0]}]: Passed\n") 129 | finally: 130 | sock.close() 131 | 132 | 133 | if __name__ == "__main__": 134 | execute_queries( 135 | sys.argv[1], 136 | when="1577836860", # 2020-01-01 00:01:00 UTC 137 | queries=[ 138 | (("sender@example.com", "recipient@example.com"), (None, None)), 139 | ( 140 | ("sender@otherdomain.com", "recipient@example.com"), 141 | ("SRS0=9KJ+=2W=otherdomain.com=sender@example.com", None), 142 | ), 143 | ( 144 | ("", "SRS0=9KJ+=2W=otherdomain.com=sender@example.com"), 145 | (None, "sender@otherdomain.com"), 146 | ), 147 | ], 148 | ) 149 | -------------------------------------------------------------------------------- /tests/blackbox/socketmap.py: -------------------------------------------------------------------------------- 1 | # PostSRSd - Sender Rewriting Scheme daemon for Postfix 2 | # Copyright 2012-2023 Timo Röhling 3 | # SPDX-License-Identifier: GPL-3.0-only 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, version 3. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | import contextlib 18 | import os 19 | import pathlib 20 | import signal 21 | import socket 22 | import stat 23 | import subprocess 24 | import sys 25 | import tempfile 26 | import time 27 | 28 | 29 | class SockStream: 30 | def __init__(self, sock): 31 | self._sock = sock 32 | self._rdbuf = b"" 33 | 34 | def read(self, size): 35 | result = b"" 36 | remaining = size 37 | while remaining > len(self._rdbuf): 38 | result += self._rdbuf 39 | remaining -= len(self._rdbuf) 40 | self._rdbuf = self._sock.recv(4096) 41 | if len(self._rdbuf) == 0: 42 | raise ConnectionError("no data") 43 | result += self._rdbuf[:remaining] 44 | self._rdbuf = self._rdbuf[remaining:] 45 | return result 46 | 47 | def write(self, data): 48 | self._sock.sendall(data) 49 | 50 | 51 | def write_netstring(sock_stream, data): 52 | data_bytes = data.encode() 53 | sock_stream.write(f"{len(data_bytes)}:".encode() + data_bytes + b",") 54 | 55 | 56 | def read_netstring(sock_stream): 57 | digit = sock_stream.read(1) 58 | data_size = 0 59 | while digit >= b"0" and digit <= b"9": 60 | data_size = 10 * data_size + int(digit) 61 | digit = sock_stream.read(1) 62 | if digit != b":": 63 | print("ERR: ':' expected") 64 | return None 65 | data = sock_stream.read(data_size) 66 | comma = sock_stream.read(1) 67 | if comma != b",": 68 | print("ERR: ',' expected") 69 | return None 70 | return data.decode() 71 | 72 | 73 | @contextlib.contextmanager 74 | def postsrsd_instance(postsrsd, when, use_database): 75 | with tempfile.TemporaryDirectory() as tmpdirname: 76 | tmpdir = pathlib.Path(tmpdirname) 77 | with open(tmpdir / "postsrsd.conf", "w") as f: 78 | f.write( 79 | 'domains = {"example.com"}\n' 80 | "keep-alive = 10\n" 81 | 'chroot-dir = ""\n' 82 | 'unprivileged-user = ""\n' 83 | f'original-envelope = {"database" if use_database else "embedded"}\n' 84 | f'socketmap = unix:{tmpdir / "postsrsd.sock"}\n' 85 | f'secrets-file = {tmpdir / "postsrsd.secret"}\n' 86 | f'envelope-database = sqlite:{tmpdir / "postsrsd.db"}\n' 87 | ) 88 | with open(tmpdir / "postsrsd.secret", "w") as f: 89 | f.write("tops3cr3t\n") 90 | os.environ["POSTSRSD_FAKETIME"] = when 91 | proc = subprocess.Popen( 92 | [postsrsd, "-C", str(tmpdir / "postsrsd.conf")], 93 | start_new_session=True, 94 | ) 95 | wait = 50 96 | while not (tmpdir / "postsrsd.sock").exists() and wait > 0: 97 | time.sleep(0.1) 98 | wait -= 1 99 | try: 100 | yield str(tmpdir / "postsrsd.sock").encode() 101 | finally: 102 | os.killpg(os.getpgid(proc.pid), signal.SIGTERM) 103 | proc.wait() 104 | 105 | 106 | def execute_queries(postsrsd, when, use_database, queries): 107 | with postsrsd_instance(postsrsd, when, use_database) as endpoint: 108 | st = os.stat(endpoint) 109 | assert st.st_mode & 0o777 == 0o666 110 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM, 0) 111 | sock.connect(endpoint) 112 | sock_stream = SockStream(sock) 113 | try: 114 | for nr, query in enumerate(queries, start=1): 115 | write_netstring(sock_stream, query[0]) 116 | result = read_netstring(sock_stream) 117 | if result != query[1]: 118 | raise AssertionError( 119 | f"query[{query[0]}]: FAILED: Expected reply {query[1]!r}, got: {result!r}" 120 | ) 121 | sys.stderr.write(f"query[{query[0]}]: Passed\n") 122 | finally: 123 | sock.close() 124 | 125 | 126 | def execute_death_tests(postsrsd, when, use_database, queries): 127 | with postsrsd_instance(postsrsd, when, use_database) as endpoint: 128 | for nr, query in enumerate(queries, start=1): 129 | try: 130 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM, 0) 131 | sock.settimeout(10) 132 | sock.connect(endpoint) 133 | sock_stream = SockStream(sock) 134 | sock_stream.write(query) 135 | result = read_netstring(sock_stream) 136 | if result != "PERM Invalid query.": 137 | raise AssertionError( 138 | f"death_test[{query}]: FAILED: Expected reply 'PERM Invalid query.', got: {result!r}" 139 | ) 140 | try: 141 | write_netstring(sock_stream, "forward test@example.com") 142 | result = read_netstring(sock_stream) 143 | raise AssertionError( 144 | f"death_test[{query}]: FAILED: Expected connection closed, got: {result!r}" 145 | ) 146 | except ConnectionError: 147 | # Expected behavior 148 | pass 149 | sys.stderr.write(f"death_test[{query}]: Passed\n") 150 | finally: 151 | sock.close() 152 | 153 | 154 | if __name__ == "__main__": 155 | execute_queries( 156 | sys.argv[1], 157 | when="1577836860", # 2020-01-01 00:01:00 UTC 158 | use_database=False, 159 | queries=[ 160 | # No rewrite for local domain 161 | ("forward test@example.com", "NOTFOUND Need not rewrite local domain."), 162 | # Regular rewrite 163 | ( 164 | "forward test@otherdomain.com", 165 | "OK SRS0=vmyz=2W=otherdomain.com=test@example.com", 166 | ), 167 | # No rerwite for mail address without domain 168 | ("forward foo", "NOTFOUND No domain."), 169 | # No rewrite for SRS address which is already in the local domain 170 | ( 171 | "forward SRS0=XjO9=2V=otherdomain.com=test@example.com", 172 | "NOTFOUND Need not rewrite local domain.", 173 | ), 174 | # Convert foreign SRS0 address to SRS1 address 175 | ( 176 | "forward SRS0=opaque+string@otherdomain.com", 177 | "OK SRS1=chaI=otherdomain.com==opaque+string@example.com", 178 | ), 179 | # Change domain part of foreign SRS1 address 180 | ( 181 | "forward SRS1=X=thirddomain.com==opaque+string@otherdomain.com", 182 | "OK SRS1=JIBX=thirddomain.com==opaque+string@example.com", 183 | ), 184 | # Recover original mail address from valid SRS0 address 185 | ( 186 | "reverse SRS0=XjO9=2V=otherdomain.com=test@example.com", 187 | "OK test@otherdomain.com", 188 | ), 189 | # Recover original SRS0 address from valid SRS1 address 190 | ( 191 | "reverse SRS1=JIBX=thirddomain.com==opaque+string@example.com", 192 | "OK SRS0=opaque+string@thirddomain.com", 193 | ), 194 | # Do not rewrite mail address which is not an SRS address 195 | ( 196 | "reverse test@example.com", 197 | "NOTFOUND Not an SRS address.", 198 | ), 199 | # Reject valid SRS0 address with time stamp older than 6 months 200 | ( 201 | "reverse SRS0=te87=T7=otherdomain.com=test@example.com", 202 | "NOTFOUND Time stamp out of date.", 203 | ), 204 | # Reject valid SRS0 address with time stamp 6 month in the future 205 | ( 206 | "reverse SRS0=VcIb=7N=otherdomain.com=test@example.com", 207 | "NOTFOUND Time stamp out of date.", 208 | ), 209 | # Reject SRS0 address with invalid hash 210 | ( 211 | "reverse SRS0=FAKE=2V=otherdomain.com=test@example.com", 212 | "NOTFOUND Hash invalid in SRS address.", 213 | ), 214 | # Recover mail address from all-lowercase SRS0 address 215 | ( 216 | "reverse srs0=xjo9=2v=otherdomain.com=test@example.com", 217 | "OK test@otherdomain.com", 218 | ), 219 | # Recover mail address from all-uppcase SRS0 address 220 | ( 221 | "reverse SRS0=XJO9=2V=OTHERDOMAIN.COM=TEST@EXAMPLE.COM", 222 | "OK TEST@OTHERDOMAIN.COM", 223 | ), 224 | # Reject SRS0 address without authenticating hash 225 | ( 226 | "reverse SRS0=@example.com", 227 | "NOTFOUND No hash in SRS0 address.", 228 | ), 229 | # Reject SRS0 address without time stamp 230 | ( 231 | "reverse SRS0=XjO9@example.com", 232 | "NOTFOUND No timestamp in SRS0 address.", 233 | ), 234 | # Reject SRS0 address without original domain 235 | ( 236 | "reverse SRS0=XjO9=2V@example.com", 237 | "NOTFOUND No host in SRS0 address.", 238 | ), 239 | # Reject SRS0 address without original localpart 240 | ( 241 | "reverse SRS0=XjO9=2V=otherdomain.com@example.com", 242 | "NOTFOUND No user in SRS0 address.", 243 | ), 244 | # Reject Database alias 245 | ( 246 | "reverse SRS0=bxzH=2W=1=DCJGDE6N24LCRT41A4T0G1UIF0DTKKQJ@example.com", 247 | "PERM No database for alias.", 248 | ), 249 | # Reject invalid socketmap 250 | ( 251 | "test@example.com", 252 | "PERM Invalid map.", 253 | ), 254 | # Test long address 255 | ( 256 | ("forward test@" + "a" * (512 - 9) + ".net"), 257 | ("OK SRS0=G7tR=2W=" + "a" * (512 - 9) + ".net=test@example.com"), 258 | ), 259 | # Recover long address 260 | ( 261 | ("reverse SRS0=iCvJ=2W=" + "a" * (512 - 34) + ".net=test@example.com"), 262 | ("OK test@" + "a" * (512 - 34) + ".net"), 263 | ), 264 | # Test too long address 265 | ( 266 | ("forward test@" + "a" * (513 - 9) + ".net"), 267 | "PERM Too big.", 268 | ), 269 | # Test empty address 270 | ( 271 | "forward ", 272 | "NOTFOUND No domain.", 273 | ), 274 | # Test empty quotes 275 | ( 276 | 'forward ""', 277 | "NOTFOUND No domain.", 278 | ), 279 | ], 280 | ) 281 | execute_death_tests( 282 | sys.argv[1], 283 | when="1577836860", # 2020-01-01 00:01:00 UTC 284 | use_database=False, 285 | queries=[ 286 | # Empty query 287 | b"0:,", 288 | # Netstring that exceeds the allowed length 289 | (b"1024:forward " + b"a" * 1016 + b","), 290 | # Old-style TCP table query 291 | b"get test@example.com\n", 292 | # Excessively large netstring length 293 | b"18446744073709551616:some data...", 294 | # Invalid netstring terminator 295 | b"28:forward test@otherdomain.com;", 296 | ], 297 | ) 298 | if sys.argv[2] == "1": 299 | execute_queries( 300 | sys.argv[1], 301 | when="1577836860", # 2020-01-01 00:01:00 UTC 302 | use_database=True, 303 | queries=[ 304 | # Regular rewrite 305 | ( 306 | "forward test@otherdomain.com", 307 | "OK SRS0=bxzH=2W=1=DCJGDE6N24LCRT41A4T0G1UIF0DTKKQJ@example.com", 308 | ), 309 | # Recover address from alias 310 | ( 311 | "reverse SRS0=bxzH=2W=1=DCJGDE6N24LCRT41A4T0G1UIF0DTKKQJ@example.com", 312 | "OK test@otherdomain.com", 313 | ), 314 | # Recover address from case-munged alias 315 | ( 316 | "reverse SRS0=bxzH=2W=1=dcjgde6n24lcrt41a4t0g1uif0dtkkqj@example.com", 317 | "OK test@otherdomain.com", 318 | ), 319 | # Reject unknown alias 320 | ( 321 | "reverse SRS0=hdxW=2W=1=VVVVVVUNVVVVVVS1VVVVVVUIVVVTKKQJ@example.com", 322 | "NOTFOUND Unknown alias.", 323 | ), 324 | # No rewrite for SRS address which is already in the local domain 325 | ( 326 | "forward SRS0=XjO9=2V=otherdomain.com=test@example.com", 327 | "NOTFOUND Need not rewrite local domain.", 328 | ), 329 | # Convert foreign SRS0 address to SRS1 address 330 | ( 331 | "forward SRS0=opaque+string@otherdomain.com", 332 | "OK SRS1=chaI=otherdomain.com==opaque+string@example.com", 333 | ), 334 | # Change domain part of foreign SRS1 address 335 | ( 336 | "forward SRS1=X=thirddomain.com==opaque+string@otherdomain.com", 337 | "OK SRS1=JIBX=thirddomain.com==opaque+string@example.com", 338 | ), 339 | # Recover original mail address from valid SRS0 address 340 | ( 341 | "reverse SRS0=XjO9=2V=otherdomain.com=test@example.com", 342 | "OK test@otherdomain.com", 343 | ), 344 | ], 345 | ) 346 | sys.exit(0) 347 | -------------------------------------------------------------------------------- /tests/unit/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # PostSRSd - Sender Rewriting Scheme daemon for Postfix 2 | # Copyright 2012-2023 Timo Röhling 3 | # SPDX-License-Identifier: GPL-3.0-only 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, version 3. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | # 17 | macro(add_postsrsd_test name) 18 | add_executable(${name}_executable ${name}.c ${ARGN}) 19 | target_include_directories( 20 | ${name}_executable PRIVATE ${CMAKE_BINARY_DIR} ${CMAKE_SOURCE_DIR}/src 21 | ) 22 | target_compile_definitions( 23 | ${name}_executable PRIVATE _GNU_SOURCE _FILE_OFFSET_BITS=64 24 | ) 25 | target_link_libraries(${name}_executable PRIVATE Check::check) 26 | target_compile_features(${name}_executable PRIVATE c_std_99) 27 | if(TESTS_WITH_ASAN) 28 | target_compile_options( 29 | ${name}_executable PRIVATE -fsanitize=address,undefined 30 | ) 31 | target_link_options( 32 | ${name}_executable PRIVATE -fsanitize=address,undefined 33 | ) 34 | endif() 35 | add_test(NAME ${name} COMMAND ${name}_executable) 36 | endmacro() 37 | 38 | set(SRCDIR ../../src) 39 | 40 | add_postsrsd_test(test_netstring ${SRCDIR}/netstring.c) 41 | add_postsrsd_test(test_sha1 ${SRCDIR}/sha1.c) 42 | add_postsrsd_test(test_util ${SRCDIR}/util.c) 43 | add_postsrsd_test(test_database ${SRCDIR}/database.c ${SRCDIR}/util.c) 44 | target_link_libraries( 45 | test_database_executable PRIVATE $<$:sqlite3::sqlite3> 46 | $<$:${HIREDIS_TARGET}> 47 | ) 48 | add_postsrsd_test(test_srs2 ${SRCDIR}/srs2.c ${SRCDIR}/sha1.c) 49 | add_postsrsd_test( 50 | test_config ${SRCDIR}/config.c ${SRCDIR}/sha1.c ${SRCDIR}/srs2.c 51 | ${SRCDIR}/util.c 52 | ) 53 | target_link_libraries(test_config_executable PRIVATE Confuse::Confuse) 54 | -------------------------------------------------------------------------------- /tests/unit/common.h: -------------------------------------------------------------------------------- 1 | /* PostSRSd - Sender Rewriting Scheme daemon for Postfix 2 | * Copyright 2012-2022 Timo Röhling 3 | * SPDX-License-Identifier: GPL-3.0-only 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #ifndef TEST_COMMON_H 18 | #define TEST_COMMON_H 19 | 20 | #include 21 | 22 | #define BEGIN_TEST_SUITE(suite) \ 23 | static Suite* suite##_suite() \ 24 | { \ 25 | Suite* ts = suite_create(#suite); \ 26 | TCase* tcase = tcase_create("tcase"); \ 27 | suite_add_tcase(ts, tcase); 28 | 29 | #define ADD_TEST_CASE(tcase) \ 30 | TCase* tcase = tcase_create(#tcase); \ 31 | suite_add_tcase(ts, tcase); 32 | 33 | #define ADD_TEST_CASE_WITH_UNCHECKED_FIXTURE(tcase, setup, teardown) \ 34 | ADD_TEST_CASE(tcase) \ 35 | tcase_add_checked_fixture(tcase, setup, teardown); 36 | 37 | #define ADD_TEST_TO_TEST_CASE(tcase, testfunc) tcase_add_test(tcase, testfunc); 38 | 39 | #define ADD_TEST(testfunc) ADD_TEST_TO_TEST_CASE(tcase, testfunc) 40 | 41 | #define END_TEST_SUITE() \ 42 | return ts; \ 43 | } 44 | 45 | #define TEST_MAIN(suite) \ 46 | int main() \ 47 | { \ 48 | Suite* s = suite##_suite(); \ 49 | SRunner* sr = srunner_create(s); \ 50 | \ 51 | srunner_run_all(sr, CK_VERBOSE); \ 52 | int number_failed = srunner_ntests_failed(sr); \ 53 | srunner_free(sr); \ 54 | return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE; \ 55 | } 56 | 57 | #endif 58 | -------------------------------------------------------------------------------- /tests/unit/test_config.c: -------------------------------------------------------------------------------- 1 | /* PostSRSd - Sender Rewriting Scheme daemon for Postfix 2 | * Copyright 2012-2024 Timo Röhling 3 | * SPDX-License-Identifier: GPL-3.0-only 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #include "common.h" 18 | #include "config.h" 19 | 20 | #include 21 | #include 22 | #include 23 | 24 | static char pwd[500]; 25 | static char tmpdir[sizeof(pwd) + 7]; 26 | 27 | void setup_fs() 28 | { 29 | ck_assert_ptr_nonnull(getcwd(pwd, sizeof(pwd))); 30 | strcpy(tmpdir, pwd); 31 | strcat(tmpdir, "/XXXXXX"); 32 | ck_assert_ptr_eq(mkdtemp(tmpdir), tmpdir); 33 | ck_assert_int_eq(chdir(tmpdir), 0); 34 | } 35 | 36 | void teardown_fs() 37 | { 38 | ck_assert_int_eq(chdir(pwd), 0); 39 | ck_assert_int_eq(rmdir(tmpdir), 0); 40 | } 41 | 42 | START_TEST(config_domains_file) 43 | { 44 | FILE* f = fopen("domains.txt", "w"); 45 | fprintf(f, 46 | "# This is a comment at the beginning of the file\n" 47 | "example.com\n" 48 | " # This is a comment with preceding white space\n" 49 | "\t tabspace.org\n" 50 | "\n" 51 | "commented.de # This is a comment after a domain name\n" 52 | "trailing.net "); 53 | fclose(f); 54 | cfg_t* cfg = config_defaults(); 55 | cfg_setstr(cfg, "domains-file", "domains.txt"); 56 | 57 | domain_set_t* D = NULL; 58 | char* srs_domain = NULL; 59 | ck_assert_int_eq(srs_domains_from_config(cfg, &srs_domain, &D), true); 60 | ck_assert_int_eq(domain_set_contains(D, "commented.de"), true); 61 | ck_assert_int_eq(domain_set_contains(D, "example.com"), true); 62 | ck_assert_int_eq(domain_set_contains(D, "tabspace.org"), true); 63 | ck_assert_int_eq(domain_set_contains(D, "trailing.net"), true); 64 | ck_assert_str_eq(srs_domain, "example.com"); 65 | ck_assert_int_eq(unlink("domains.txt"), 0); 66 | domain_set_destroy(D); 67 | free(srs_domain); 68 | cfg_free(cfg); 69 | } 70 | END_TEST 71 | 72 | BEGIN_TEST_SUITE(config) 73 | ADD_TEST_CASE_WITH_UNCHECKED_FIXTURE(fs, setup_fs, teardown_fs) 74 | ADD_TEST_TO_TEST_CASE(fs, config_domains_file) 75 | END_TEST_SUITE() 76 | TEST_MAIN(config) 77 | -------------------------------------------------------------------------------- /tests/unit/test_database.c: -------------------------------------------------------------------------------- 1 | /* PostSRSd - Sender Rewriting Scheme daemon for Postfix 2 | * Copyright 2012-2022 Timo Röhling 3 | * SPDX-License-Identifier: GPL-3.0-only 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #include "common.h" 18 | #include "database.h" 19 | 20 | #include 21 | #include 22 | #include 23 | #ifdef HAVE_UNISTD_H 24 | # include 25 | #endif 26 | 27 | START_TEST(invalid_database) 28 | { 29 | ck_assert_ptr_null(database_connect("invalid:", true)); 30 | } 31 | END_TEST 32 | 33 | #ifdef WITH_SQLITE 34 | START_TEST(database_sqlite_key_value) 35 | { 36 | database_t* db = database_connect("sqlite::memory:", true); 37 | ck_assert_ptr_nonnull(db); 38 | ck_assert_ptr_null(database_read(db, "mykey")); 39 | database_write(db, "mykey", "myvalue", 1); 40 | char* value = database_read(db, "mykey"); 41 | ck_assert_str_eq(value, "myvalue"); 42 | free(value); 43 | database_disconnect(db); 44 | } 45 | END_TEST 46 | 47 | START_TEST(database_sqlite_expiry) 48 | { 49 | database_t* db = database_connect("sqlite::memory:", true); 50 | ck_assert_ptr_nonnull(db); 51 | database_write(db, "mykey", "myvalue", 0); 52 | char* value = database_read(db, "mykey"); 53 | ck_assert_str_eq(value, "myvalue"); 54 | free(value); 55 | database_expire(db); 56 | ck_assert_ptr_null(database_read(db, "mykey")); 57 | database_disconnect(db); 58 | } 59 | END_TEST 60 | #endif 61 | 62 | #ifdef WITH_REDIS 63 | START_TEST(database_redis_key_value) 64 | { 65 | database_t* db = database_connect("redis:localhost:6379", true); 66 | if (db == NULL) 67 | return; /* skip test if no redis server is available */ 68 | database_write(db, "mykey", "myvalue", 1); 69 | char* value = database_read(db, "mykey"); 70 | ck_assert_str_eq(value, "myvalue"); 71 | free(value); 72 | database_disconnect(db); 73 | } 74 | END_TEST 75 | 76 | START_TEST(database_redis_expiry) 77 | { 78 | database_t* db = database_connect("redis:localhost:6379", true); 79 | if (db == NULL) 80 | return; /* skip test if no redis server is available */ 81 | database_write(db, "mykey", "myvalue", 1); 82 | char* value = database_read(db, "mykey"); 83 | ck_assert_str_eq(value, "myvalue"); 84 | free(value); 85 | sleep(2); 86 | database_expire(db); 87 | ck_assert_ptr_null(database_read(db, "mykey")); 88 | database_disconnect(db); 89 | } 90 | END_TEST 91 | #endif 92 | 93 | BEGIN_TEST_SUITE(database) 94 | ADD_TEST(invalid_database) 95 | #ifdef WITH_SQLITE 96 | ADD_TEST(database_sqlite_key_value) 97 | ADD_TEST(database_sqlite_expiry) 98 | #endif 99 | #ifdef WITH_REDIS 100 | ADD_TEST(database_redis_key_value) 101 | ADD_TEST(database_redis_expiry) 102 | #endif 103 | END_TEST_SUITE() 104 | TEST_MAIN(database) 105 | -------------------------------------------------------------------------------- /tests/unit/test_netstring.c: -------------------------------------------------------------------------------- 1 | /* PostSRSd - Sender Rewriting Scheme daemon for Postfix 2 | * Copyright 2012-2023 Timo Röhling 3 | * SPDX-License-Identifier: GPL-3.0-only 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #include "common.h" 18 | #include "netstring.h" 19 | 20 | #include 21 | #include 22 | #include 23 | 24 | START_TEST(netstring_encode_test) 25 | { 26 | char buffer[16]; 27 | char* result; 28 | size_t length; 29 | 30 | result = netstring_encode("PostSRSd", 8, buffer, sizeof(buffer), &length); 31 | ck_assert_ptr_nonnull(result); 32 | ck_assert_uint_eq(length, 11); 33 | ck_assert_mem_eq(result, "8:PostSRSd,", length); 34 | 35 | result = 36 | netstring_encode("ItBarelyFits", 12, buffer, sizeof(buffer), &length); 37 | ck_assert_ptr_nonnull(result); 38 | ck_assert_uint_eq(length, 16); 39 | ck_assert_mem_eq(result, "12:ItBarelyFits,", length); 40 | 41 | result = 42 | netstring_encode("ItDoesNotFit!", 13, buffer, sizeof(buffer), &length); 43 | ck_assert_ptr_null(result); 44 | 45 | result = netstring_encode(NULL, 0, buffer, sizeof(buffer), &length); 46 | ck_assert_ptr_null(result); 47 | 48 | result = netstring_encode("", 0, buffer, sizeof(buffer), &length); 49 | ck_assert_ptr_nonnull(result); 50 | ck_assert_uint_eq(length, 3); 51 | ck_assert_mem_eq(result, "0:,", length); 52 | } 53 | END_TEST 54 | 55 | START_TEST(netstring_decode_test) 56 | { 57 | char buffer[17]; 58 | char* result; 59 | size_t length; 60 | 61 | result = netstring_decode("8:PostSRSd,", buffer, sizeof(buffer), &length); 62 | ck_assert_ptr_nonnull(result); 63 | ck_assert_uint_eq(length, 8); 64 | ck_assert_mem_eq(result, "PostSRSd", length); 65 | 66 | result = netstring_decode("16:0123456789abcdef,", buffer, sizeof(buffer), 67 | &length); 68 | ck_assert_ptr_nonnull(result); 69 | ck_assert_uint_eq(length, 16); 70 | ck_assert_mem_eq(result, "0123456789abcdef", length); 71 | 72 | result = netstring_decode("0:,", buffer, sizeof(buffer), &length); 73 | ck_assert_ptr_nonnull(result); 74 | ck_assert_uint_eq(length, 0); 75 | 76 | result = netstring_decode(NULL, buffer, sizeof(buffer), &length); 77 | ck_assert_ptr_null(result); 78 | 79 | result = netstring_decode("1a,", buffer, sizeof(buffer), &length); 80 | ck_assert_ptr_null(result); 81 | 82 | result = netstring_decode("1:a*", buffer, sizeof(buffer), &length); 83 | ck_assert_ptr_null(result); 84 | 85 | result = netstring_decode("0x1:a,", buffer, sizeof(buffer), &length); 86 | ck_assert_ptr_null(result); 87 | 88 | result = netstring_decode("000001:a,", buffer, sizeof(buffer), &length); 89 | ck_assert_ptr_null(result); 90 | } 91 | END_TEST 92 | 93 | START_TEST(netstring_io_test) 94 | { 95 | int written; 96 | char* data; 97 | char buffer[16]; 98 | size_t length; 99 | FILE* f = tmpfile(); 100 | 101 | written = netstring_write(f, "PostSRSd", 8); 102 | ck_assert_int_eq(written, 11); 103 | written = netstring_write(f, "", 0); 104 | ck_assert_int_eq(written, 3); 105 | written = netstring_write(f, "0123456789abcdefgh", 17); 106 | ck_assert_int_eq(written, 21); 107 | 108 | ck_assert_int_eq(fseek(f, 0, SEEK_SET), 0); 109 | 110 | data = netstring_read(f, buffer, sizeof(buffer), &length); 111 | ck_assert_ptr_nonnull(data); 112 | ck_assert_uint_eq(length, 8); 113 | ck_assert_mem_eq(data, "PostSRSd", length); 114 | 115 | data = netstring_read(f, buffer, sizeof(buffer), &length); 116 | ck_assert_ptr_nonnull(data); 117 | ck_assert_uint_eq(length, 0); 118 | 119 | data = netstring_read(f, buffer, sizeof(buffer), &length); 120 | ck_assert_ptr_null(data); 121 | 122 | ck_assert_int_eq(fseek(f, 0, SEEK_SET), 0); 123 | ck_assert_int_eq(ftruncate(fileno(f), 0), 0); 124 | fwrite("3:abc,4:abcde", 1, 13, f); 125 | 126 | ck_assert_int_eq(fseek(f, 0, SEEK_SET), 0); 127 | data = netstring_read(f, buffer, sizeof(buffer), &length); 128 | ck_assert_ptr_nonnull(data); 129 | ck_assert_uint_eq(length, 3); 130 | ck_assert_mem_eq(data, "abc", length); 131 | 132 | data = netstring_read(f, buffer, sizeof(buffer), &length); 133 | ck_assert_ptr_null(data); 134 | 135 | ck_assert_int_eq(fseek(f, 0, SEEK_SET), 0); 136 | ck_assert_int_eq(ftruncate(fileno(f), 0), 0); 137 | fwrite("999:obviously too short,", 1, 4, f); 138 | 139 | ck_assert_int_eq(fseek(f, 0, SEEK_SET), 0); 140 | data = netstring_read(f, buffer, sizeof(buffer), &length); 141 | ck_assert_ptr_null(data); 142 | fclose(f); 143 | } 144 | END_TEST 145 | 146 | BEGIN_TEST_SUITE(netstring) 147 | ADD_TEST(netstring_encode_test) 148 | ADD_TEST(netstring_decode_test) 149 | ADD_TEST(netstring_io_test) 150 | END_TEST_SUITE() 151 | TEST_MAIN(netstring) 152 | -------------------------------------------------------------------------------- /tests/unit/test_sha1.c: -------------------------------------------------------------------------------- 1 | /* PostSRSd - Sender Rewriting Scheme daemon for Postfix 2 | * Copyright 2012-2022 Timo Röhling 3 | * SPDX-License-Identifier: GPL-3.0-only 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #include "common.h" 18 | #include "sha1.h" 19 | 20 | #include 21 | #include 22 | 23 | START_TEST(sha1_test_vectors) 24 | { 25 | char digest[20]; 26 | sha_digest(digest, "", 0); 27 | ck_assert_mem_eq(digest, 28 | "\xda\x39\xa3\xee\x5e\x6b\x4b\x0d\x32\x55\xbf\xef\x95\x60" 29 | "\x18\x90\xaf\xd8\x07\x09", 30 | 20); 31 | sha_digest(digest, "abc", 3); 32 | ck_assert_mem_eq(digest, 33 | "\xa9\x99\x3e\x36\x47\x06\x81\x6a\xba\x3e\x25\x71\x78\x50" 34 | "\xc2\x6c\x9c\xd0\xd8\x9d", 35 | 20); 36 | char* one_million_a = (char*)malloc(1000000); 37 | ck_assert_ptr_nonnull(one_million_a); 38 | memset(one_million_a, 'a', 1000000); 39 | sha_digest(digest, one_million_a, 1000000); 40 | ck_assert_mem_eq(digest, 41 | "\x34\xaa\x97\x3c\xd4\xc4\xda\xa4\xf6\x1e\xeb\x2b\xdb\xad" 42 | "\x27\x31\x65\x34\x01\x6f", 43 | 20); 44 | free(one_million_a); 45 | } 46 | END_TEST 47 | 48 | START_TEST(hmac_sha1_test_vectors) 49 | { 50 | srs_hmac_ctx_t ctx; 51 | char digest[20]; 52 | srs_hmac_init(&ctx, "topsecret", 9); 53 | srs_hmac_update(&ctx, "PostSRSd", 8); 54 | srs_hmac_fini(&ctx, digest); 55 | ck_assert_mem_eq(digest, 56 | "\xc7\xaa\x15\x9e\x2d\x8f\x36\x6d\xe9\x50\xe5\xf9\x36\xdc" 57 | "\x7f\x65\xea\x50\xd1\x13", 58 | 20); 59 | srs_hmac_init(&ctx, "Jefe", 4); 60 | srs_hmac_update(&ctx, "what do ya want for nothing?", 28); 61 | srs_hmac_fini(&ctx, digest); 62 | ck_assert_mem_eq(digest, 63 | "\xef\xfc\xdf\x6a\xe5\xeb\x2f\xa2\xd2\x74\x16\xd5\xf1\x84" 64 | "\xdf\x9c\x25\x9a\x7c\x79", 65 | 20); 66 | srs_hmac_init( 67 | &ctx, 68 | "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" 69 | "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" 70 | "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" 71 | "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa" 72 | "\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa", 73 | 80); 74 | srs_hmac_update(&ctx, 75 | "Test Using Larger Than Block-Size Key and Larger Than One " 76 | "Block-Size Data", 77 | 73); 78 | srs_hmac_fini(&ctx, digest); 79 | ck_assert_mem_eq(digest, 80 | "\xe8\xe9\x9d\x0f\x45\x23\x7d\x78\x6d\x6b\xba\xa7\x96\x5c" 81 | "\x78\x08\xbb\xff\x1a\x91", 82 | 20); 83 | } 84 | 85 | BEGIN_TEST_SUITE(sha1) 86 | ADD_TEST(sha1_test_vectors) 87 | ADD_TEST(hmac_sha1_test_vectors) 88 | END_TEST_SUITE() 89 | TEST_MAIN(sha1) 90 | -------------------------------------------------------------------------------- /tests/unit/test_srs2.c: -------------------------------------------------------------------------------- 1 | /* PostSRSd - Sender Rewriting Scheme daemon for Postfix 2 | * Copyright 2012-2023 Timo Röhling 3 | * SPDX-License-Identifier: GPL-3.0-only 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #include "common.h" 18 | #include "srs2.h" 19 | 20 | #include 21 | 22 | srs_t* create_srs_t() 23 | { 24 | srs_t* srs = srs_new(); 25 | srs->faketime = 1577836860; /* 2020-01-01 00:01:00 UTC */ 26 | srs_add_secret(srs, "tops3cr3t"); 27 | return srs; 28 | } 29 | 30 | START_TEST(srs2_forwarding) 31 | { 32 | srs_t* srs = create_srs_t(); 33 | char* output = NULL; 34 | int result; 35 | 36 | result = srs_forward_alloc(srs, &output, "test@example.com", "example.com"); 37 | ck_assert_int_eq(result, SRS_SUCCESS); 38 | ck_assert_str_eq(output, "test@example.com"); 39 | free(output); 40 | 41 | result = 42 | srs_forward_alloc(srs, &output, "test@otherdomain.com", "example.com"); 43 | ck_assert_int_eq(result, SRS_SUCCESS); 44 | ck_assert_str_eq(output, "SRS0=vmyz=2W=otherdomain.com=test@example.com"); 45 | free(output); 46 | 47 | result = srs_forward_alloc(srs, &output, "foo", "example.com"); 48 | ck_assert_int_eq(result, SRS_ENOSENDERATSIGN); 49 | 50 | srs_free(srs); 51 | } 52 | END_TEST 53 | 54 | START_TEST(srs2_reversing) 55 | { 56 | srs_t* srs = create_srs_t(); 57 | char* output = NULL; 58 | int result; 59 | 60 | result = srs_reverse_alloc(srs, &output, "test@example.com"); 61 | ck_assert_int_eq(result, SRS_ENOTSRSADDRESS); 62 | 63 | result = srs_reverse_alloc(srs, &output, 64 | "SRS0=vmyz=2W=otherdomain.com=test@example.com"); 65 | ck_assert_int_eq(result, SRS_SUCCESS); 66 | ck_assert_str_eq(output, "test@otherdomain.com"); 67 | free(output); 68 | 69 | srs_free(srs); 70 | } 71 | END_TEST 72 | 73 | BEGIN_TEST_SUITE(srs2) 74 | ADD_TEST(srs2_forwarding); 75 | ADD_TEST(srs2_reversing); 76 | END_TEST_SUITE() 77 | TEST_MAIN(srs2) 78 | -------------------------------------------------------------------------------- /tests/unit/test_util.c: -------------------------------------------------------------------------------- 1 | /* PostSRSd - Sender Rewriting Scheme daemon for Postfix 2 | * Copyright 2012-2022 Timo Röhling 3 | * SPDX-License-Identifier: GPL-3.0-only 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, version 3. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #include "common.h" 18 | #include "postsrsd_build_config.h" 19 | #include "util.h" 20 | 21 | #include 22 | #include 23 | #ifdef HAVE_SYS_FILE_H 24 | # include 25 | #endif 26 | #ifdef HAVE_SYS_STAT_H 27 | # include 28 | #endif 29 | #include 30 | 31 | static char pwd[500]; 32 | static char tmpdir[sizeof(pwd) + 7]; 33 | 34 | void setup_fs() 35 | { 36 | ck_assert_ptr_nonnull(getcwd(pwd, sizeof(pwd))); 37 | strcpy(tmpdir, pwd); 38 | strcat(tmpdir, "/XXXXXX"); 39 | ck_assert_ptr_eq(mkdtemp(tmpdir), tmpdir); 40 | ck_assert_int_eq(chdir(tmpdir), 0); 41 | } 42 | 43 | void teardown_fs() 44 | { 45 | ck_assert_int_eq(chdir(pwd), 0); 46 | ck_assert_int_eq(rmdir(tmpdir), 0); 47 | } 48 | 49 | START_TEST(util_file_exists) 50 | { 51 | ck_assert(!file_exists("testfile")); 52 | ck_assert(!file_exists("testdir")); 53 | 54 | ck_assert_int_eq(mkdir("testdir", 0755), 0); 55 | FILE* f = fopen("testfile", "w"); 56 | fwrite("Test", 4, 1, f); 57 | fclose(f); 58 | 59 | ck_assert(file_exists("testfile")); 60 | ck_assert(!file_exists("testdir")); 61 | 62 | ck_assert_int_eq(unlink("testfile"), 0); 63 | ck_assert_int_eq(rmdir("testdir"), 0); 64 | 65 | ck_assert(!file_exists("testfile")); 66 | } 67 | END_TEST 68 | 69 | START_TEST(util_directory_exists) 70 | { 71 | ck_assert(!directory_exists("testfile")); 72 | ck_assert(!directory_exists("testdir")); 73 | 74 | ck_assert_int_eq(mkdir("testdir", 0755), 0); 75 | FILE* f = fopen("testfile", "w"); 76 | fwrite("Test", 4, 1, f); 77 | fclose(f); 78 | 79 | ck_assert(directory_exists("testdir")); 80 | ck_assert(!directory_exists("testfile")); 81 | 82 | ck_assert_int_eq(unlink("testfile"), 0); 83 | ck_assert_int_eq(rmdir("testdir"), 0); 84 | 85 | ck_assert(!directory_exists("testdir")); 86 | } 87 | END_TEST 88 | 89 | START_TEST(util_set_string) 90 | { 91 | char* s = NULL; 92 | set_string(&s, strdup("Test")); 93 | ck_assert_str_eq(s, "Test"); 94 | set_string(&s, NULL); 95 | ck_assert_ptr_null(s); 96 | } 97 | END_TEST 98 | 99 | START_TEST(util_argvdup) 100 | { 101 | const char* tokens[5] = {"one", "two", "three", "four", "five"}; 102 | char* argv[5]; 103 | ck_assert_ptr_null(argvdup(NULL)); 104 | for (size_t num = 0; num < 5; ++num) 105 | { 106 | for (size_t i = 0; i < num; ++i) 107 | { 108 | argv[i] = strdup(tokens[i]); 109 | } 110 | argv[num] = NULL; 111 | char** result = argvdup(argv); 112 | ck_assert_ptr_ne(argv, result); 113 | for (size_t i = 0; i < num; ++i) 114 | { 115 | ck_assert_ptr_ne(argv[i], result[i]); 116 | ck_assert_str_eq(argv[i], result[i]); 117 | free(argv[i]); 118 | } 119 | ck_assert_ptr_null(result[num]); 120 | freeargv(result); 121 | } 122 | freeargv(NULL); 123 | } 124 | END_TEST 125 | 126 | START_TEST(util_strip_brackets) 127 | { 128 | char* result; 129 | result = strip_brackets("test@example.com"); 130 | ck_assert_ptr_eq(result, NULL); 131 | result = strip_brackets(""); 134 | ck_assert_ptr_eq(result, NULL); 135 | result = strip_brackets(""); 136 | ck_assert_str_eq(result, "test@example.com"); 137 | free(result); 138 | result = strip_brackets("Test User "); 139 | ck_assert_str_eq(result, "test@example.com"); 140 | free(result); 141 | } 142 | END_TEST 143 | 144 | START_TEST(util_list) 145 | { 146 | list_t* L = list_create(); 147 | ck_assert_uint_eq(list_size(L), 0); 148 | ck_assert_ptr_null(list_get(L, 0)); 149 | ck_assert_uint_eq(list_append(L, strdup("0")), true); 150 | ck_assert_uint_eq(list_size(L), 1); 151 | ck_assert_str_eq((char*)list_get(L, 0), "0"); 152 | ck_assert_ptr_null(list_get(L, 1)); 153 | ck_assert_uint_eq(list_append(L, strdup("1")), true); 154 | ck_assert_uint_eq(list_append(L, strdup("2")), true); 155 | ck_assert_uint_eq(list_append(L, strdup("3")), true); 156 | ck_assert_uint_eq(list_append(L, strdup("4")), true); 157 | ck_assert_uint_eq(list_append(L, strdup("5")), true); 158 | ck_assert_uint_eq(list_size(L), 6); 159 | ck_assert_str_eq((char*)list_get(L, 0), "0"); 160 | ck_assert_str_eq((char*)list_get(L, 1), "1"); 161 | ck_assert_str_eq((char*)list_get(L, 2), "2"); 162 | ck_assert_str_eq((char*)list_get(L, 3), "3"); 163 | ck_assert_str_eq((char*)list_get(L, 4), "4"); 164 | ck_assert_str_eq((char*)list_get(L, 5), "5"); 165 | ck_assert_ptr_null(list_get(L, 6)); 166 | list_clear(L, free); 167 | ck_assert_uint_eq(list_size(L), 0); 168 | ck_assert_uint_eq(list_append(L, strdup("a")), true); 169 | ck_assert_str_eq((char*)list_get(L, 0), "a"); 170 | list_destroy(L, free); 171 | list_destroy(NULL, free); 172 | } 173 | END_TEST 174 | 175 | START_TEST(util_b32h_encode) 176 | { 177 | char buffer[41]; 178 | char* b32h; 179 | 180 | b32h = b32h_encode("", 0, buffer, sizeof(buffer)); 181 | ck_assert_ptr_nonnull(b32h); 182 | ck_assert_str_eq(b32h, ""); 183 | 184 | b32h = b32h_encode("-PostSRSd-", 10, buffer, sizeof(buffer)); 185 | ck_assert_ptr_nonnull(b32h); 186 | ck_assert_str_eq(b32h, "5L86USRKAD956P1D"); 187 | 188 | ck_assert_ptr_null(b32h_encode("BufferTooSmall!", 15, buffer, 24)); 189 | 190 | b32h = b32h_encode("BuffLargeEnough", 15, buffer, 25); 191 | ck_assert_ptr_nonnull(b32h); 192 | ck_assert_str_eq(b32h, "89QMCPICC5P6EPA5DPNNAPR8"); 193 | 194 | b32h = b32h_encode("a", 1, buffer, sizeof(buffer)); 195 | ck_assert_ptr_nonnull(b32h); 196 | ck_assert_str_eq(b32h, "C4======"); 197 | 198 | b32h = b32h_encode("ab", 2, buffer, sizeof(buffer)); 199 | ck_assert_ptr_nonnull(b32h); 200 | ck_assert_str_eq(b32h, "C5H0===="); 201 | 202 | b32h = b32h_encode("abc", 3, buffer, sizeof(buffer)); 203 | ck_assert_ptr_nonnull(b32h); 204 | ck_assert_str_eq(b32h, "C5H66==="); 205 | 206 | b32h = b32h_encode("abcd", 4, buffer, sizeof(buffer)); 207 | ck_assert_ptr_nonnull(b32h); 208 | ck_assert_str_eq(b32h, "C5H66P0="); 209 | 210 | b32h = b32h_encode("abcde", 5, buffer, sizeof(buffer)); 211 | ck_assert_ptr_nonnull(b32h); 212 | ck_assert_str_eq(b32h, "C5H66P35"); 213 | 214 | b32h = b32h_encode("\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80", 10, buffer, 215 | sizeof(buffer)); 216 | ck_assert_ptr_nonnull(b32h); 217 | ck_assert_str_eq(b32h, "G2081040G2081040"); 218 | } 219 | END_TEST 220 | 221 | START_TEST(util_dotlock) 222 | { 223 | #if defined(LOCK_EX) && defined(LOCK_NB) 224 | for (int i = 0; i < 2; ++i) 225 | { 226 | int handle = acquire_lock("testfile"); 227 | ck_assert_int_gt(handle, 0); 228 | ck_assert_int_lt(acquire_lock("testfile"), 0); 229 | release_lock("testfile", handle); 230 | } 231 | #endif 232 | } 233 | END_TEST 234 | 235 | START_TEST(util_domain_set) 236 | { 237 | struct domain_set* D = domain_set_create(); 238 | ck_assert(!domain_set_contains(D, "example.com")); 239 | ck_assert(!domain_set_contains(D, ".example.com")); 240 | ck_assert(!domain_set_contains(D, "exam.com")); 241 | domain_set_add(D, "example.com"); 242 | domain_set_add(D, "www.example.com"); 243 | ck_assert(domain_set_contains(D, "example.com")); 244 | ck_assert(domain_set_contains(D, "EXAMPLE.COM")); 245 | ck_assert(domain_set_contains(D, "www.example.com")); 246 | ck_assert(!domain_set_contains(D, ".example.com")); 247 | ck_assert(!domain_set_contains(D, "mail.example.com")); 248 | ck_assert(!domain_set_contains(D, "exam.com")); 249 | domain_set_add(D, ".example.com"); 250 | ck_assert(domain_set_contains(D, "example.com")); 251 | ck_assert(domain_set_contains(D, ".example.com")); 252 | ck_assert(domain_set_contains(D, "www.example.com")); 253 | ck_assert(domain_set_contains(D, "mail.example.com")); 254 | ck_assert(!domain_set_contains(D, "exam.com")); 255 | domain_set_add(D, ".my-examples.com"); 256 | ck_assert(!domain_set_contains(D, "my-examples.com")); 257 | ck_assert(domain_set_contains(D, "another.one.of.my-examples.com")); 258 | domain_set_add(D, "invalid$domain.net"); 259 | ck_assert(!domain_set_contains(D, "invalid$domain.net")); 260 | domain_set_add( 261 | D, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-0123456789."); 262 | ck_assert(domain_set_contains( 263 | D, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-0123456789.")); 264 | domain_set_destroy(D); 265 | } 266 | 267 | START_TEST(util_endpoint_for_milter) 268 | { 269 | char* ep; 270 | ck_assert_ptr_null(endpoint_for_milter(NULL)); 271 | ck_assert_ptr_null(endpoint_for_milter("invalid")); 272 | ck_assert_ptr_null( 273 | endpoint_for_milter("https://this.is.not.a.valid.endpoint.net")); 274 | ck_assert_ptr_null(endpoint_for_milter("inet:host.but.no.port")); 275 | ck_assert_ptr_null(endpoint_for_milter("inet:1234")); 276 | ck_assert_ptr_null(endpoint_for_milter("inet:localhost:")); 277 | ck_assert_ptr_null(endpoint_for_milter("inet::1234")); 278 | ck_assert_ptr_null(endpoint_for_milter("inet6:1234")); 279 | ck_assert_ptr_null(endpoint_for_milter("inet6:localhost:")); 280 | ck_assert_ptr_null(endpoint_for_milter("inet6::1234")); 281 | ep = endpoint_for_milter("unix:/some/path"); 282 | ck_assert_str_eq(ep, "unix:/some/path"); 283 | free(ep); 284 | ep = endpoint_for_milter("inet:localhost:1234"); 285 | ck_assert_str_eq(ep, "inet:1234@localhost"); 286 | free(ep); 287 | ep = endpoint_for_milter("inet:*:1234"); 288 | ck_assert_str_eq(ep, "inet:1234"); 289 | free(ep); 290 | ep = endpoint_for_milter("inet6:localhost:1234"); 291 | ck_assert_str_eq(ep, "inet6:1234@localhost"); 292 | free(ep); 293 | ep = endpoint_for_milter("inet6:*:1234"); 294 | ck_assert_str_eq(ep, "inet6:1234"); 295 | free(ep); 296 | } 297 | END_TEST 298 | 299 | START_TEST(util_log) 300 | { 301 | char buffer[2049]; 302 | memset(buffer, 'a', sizeof(buffer) - 1); 303 | buffer[sizeof(buffer) - 1] = 0; 304 | log_enable_syslog(); 305 | log_info("Hello %s", "World"); 306 | log_warn("Excessively long message: %s", buffer); 307 | log_error("Error?"); 308 | } 309 | END_TEST 310 | 311 | BEGIN_TEST_SUITE(util) 312 | ADD_TEST_CASE_WITH_UNCHECKED_FIXTURE(fs, setup_fs, teardown_fs) 313 | ADD_TEST_TO_TEST_CASE(fs, util_file_exists) 314 | ADD_TEST_TO_TEST_CASE(fs, util_directory_exists) 315 | ADD_TEST_TO_TEST_CASE(fs, util_dotlock) 316 | ADD_TEST(util_set_string) 317 | ADD_TEST(util_argvdup); 318 | ADD_TEST(util_strip_brackets); 319 | ADD_TEST(util_list); 320 | ADD_TEST(util_b32h_encode) 321 | ADD_TEST(util_domain_set) 322 | ADD_TEST(util_endpoint_for_milter) 323 | ADD_TEST(util_log) 324 | END_TEST_SUITE() 325 | TEST_MAIN(util) 326 | --------------------------------------------------------------------------------