From e90fcc54809db2591dc083f43ef54c6ec8c60847 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 18:16:13 +0200 Subject: Adding upstream version 4.96. Signed-off-by: Daniel Baumann --- src/EDITME | 1497 ++++++ src/acl.c | 4896 +++++++++++++++++++ src/aliases.default | 40 + src/arc.c | 1903 ++++++++ src/auths/Makefile | 45 + src/auths/README | 98 + src/auths/auth-spa.c | 1524 ++++++ src/auths/auth-spa.h | 92 + src/auths/call_pam.c | 204 + src/auths/call_pwcheck.c | 121 + src/auths/call_radius.c | 223 + src/auths/check_serv_cond.c | 124 + src/auths/cram_md5.c | 360 ++ src/auths/cram_md5.h | 31 + src/auths/cyrus_sasl.c | 513 ++ src/auths/cyrus_sasl.h | 36 + src/auths/dovecot.c | 530 +++ src/auths/dovecot.h | 30 + src/auths/external.c | 155 + src/auths/external.h | 32 + src/auths/get_data.c | 259 + src/auths/get_no64_data.c | 47 + src/auths/gsasl_exim.c | 1024 ++++ src/auths/gsasl_exim.h | 53 + src/auths/heimdal_gssapi.c | 618 +++ src/auths/heimdal_gssapi.h | 39 + src/auths/plaintext.c | 179 + src/auths/plaintext.h | 31 + src/auths/pwcheck.c | 449 ++ src/auths/pwcheck.h | 27 + src/auths/spa.c | 376 ++ src/auths/spa.h | 38 + src/auths/tls.c | 94 + src/auths/tls.h | 30 + src/auths/xtextdecode.c | 58 + src/auths/xtextencode.c | 58 + src/base64.c | 297 ++ src/blob.h | 15 + src/bmi_spam.c | 476 ++ src/bmi_spam.h | 22 + src/buildconfig.c | 984 ++++ src/child.c | 556 +++ src/cnumber.h | 1 + src/config.h.defaults | 235 + src/configure.default | 1015 ++++ src/convert4r3.src | 1382 ++++++ src/convert4r4.src | 2527 ++++++++++ src/crypt16.c | 77 + src/daemon.c | 2654 +++++++++++ src/dane-openssl.c | 1719 +++++++ src/dane.c | 48 + src/danessl.h | 47 + src/dbfn.c | 681 +++ src/dbfunctions.h | 38 + src/dcc.c | 490 ++ src/dcc.h | 16 + src/debug.c | 498 ++ src/deliver.c | 8642 ++++++++++++++++++++++++++++++++++ src/directory.c | 95 + src/dkim.c | 895 ++++ src/dkim.h | 32 + src/dkim_transport.c | 412 ++ src/dmarc.c | 655 +++ src/dmarc.h | 60 + src/dns.c | 1332 ++++++ src/dnsbl.c | 651 +++ src/drtables.c | 818 ++++ src/dummies.c | 139 + src/enq.c | 123 + src/environment.c | 83 + src/exicyclog.src | 350 ++ src/exigrep.src | 380 ++ src/exim.c | 6097 ++++++++++++++++++++++++ src/exim.h | 671 +++ src/exim_checkaccess.src | 181 + src/exim_dbmbuild.c | 536 +++ src/exim_dbutil.c | 1423 ++++++ src/exim_lock.c | 663 +++ src/eximon.src | 221 + src/eximstats.src | 4246 +++++++++++++++++ src/exinext.src | 262 ++ src/exipick.src | 1841 ++++++++ src/exiqgrep.src | 216 + src/exiqsumm.src | 178 + src/exiwhat.src | 145 + src/expand.c | 8837 +++++++++++++++++++++++++++++++++++ src/filter.c | 2607 +++++++++++ src/filtertest.c | 282 ++ src/functions.h | 1305 ++++++ src/globals.c | 1653 +++++++ src/globals.h | 1120 +++++ src/hash.c | 895 ++++ src/hash.h | 83 + src/header.c | 468 ++ src/hintsdb.h | 810 ++++ src/hintsdb_structs.h | 189 + src/host.c | 3431 ++++++++++++++ src/imap_utf7.c | 211 + src/ip.c | 853 ++++ src/local_scan.c | 64 + src/local_scan.h | 239 + src/log.c | 1554 ++++++ src/lookupapi.h | 65 + src/lookups/Makefile | 76 + src/lookups/README | 181 + src/lookups/cdb.c | 490 ++ src/lookups/dbmdb.c | 284 ++ src/lookups/dnsdb.c | 614 +++ src/lookups/dsearch.c | 190 + src/lookups/ibase.c | 561 +++ src/lookups/json.c | 188 + src/lookups/ldap.c | 1613 +++++++ src/lookups/ldap.h | 13 + src/lookups/lf_check_file.c | 113 + src/lookups/lf_functions.h | 20 + src/lookups/lf_quote.c | 63 + src/lookups/lf_sqlperform.c | 175 + src/lookups/lmdb.c | 162 + src/lookups/lsearch.c | 487 ++ src/lookups/mysql.c | 503 ++ src/lookups/nis.c | 141 + src/lookups/nisplus.c | 294 ++ src/lookups/oracle.c | 628 +++ src/lookups/passwd.c | 85 + src/lookups/pgsql.c | 506 ++ src/lookups/readsock.c | 341 ++ src/lookups/redis.c | 471 ++ src/lookups/spf.c | 159 + src/lookups/sqlite.c | 200 + src/lookups/testdb.c | 100 + src/lookups/whoson.c | 98 + src/lss.c | 142 + src/macro_predef.c | 345 ++ src/macro_predef.h | 28 + src/macros.h | 1117 +++++ src/malware.c | 2328 +++++++++ src/match.c | 1352 ++++++ src/md5.c | 355 ++ src/memcheck.h | 277 ++ src/mime.c | 803 ++++ src/mime.h | 44 + src/moan.c | 874 ++++ src/mytypes.h | 150 + src/os.c | 971 ++++ src/osfunctions.h | 43 + src/parse.c | 2243 +++++++++ src/pdkim/Makefile | 19 + src/pdkim/README | 9 + src/pdkim/config.h | 4 + src/pdkim/crypt_ver.h | 33 + src/pdkim/pdkim.c | 2097 +++++++++ src/pdkim/pdkim.h | 369 ++ src/pdkim/pdkim_hash.h | 38 + src/pdkim/signing.c | 903 ++++ src/pdkim/signing.h | 97 + src/perl.c | 201 + src/priv.c | 76 + src/queue.c | 1588 +++++++ src/rda.c | 997 ++++ src/readconf.c | 4510 ++++++++++++++++++ src/receive.c | 4528 ++++++++++++++++++ src/regex.c | 217 + src/retry.c | 934 ++++ src/rewrite.c | 825 ++++ src/rfc2047.c | 345 ++ src/route.c | 2072 ++++++++ src/routers/Makefile | 43 + src/routers/README | 57 + src/routers/accept.c | 139 + src/routers/accept.h | 31 + src/routers/dnslookup.c | 477 ++ src/routers/dnslookup.h | 42 + src/routers/ipliteral.c | 202 + src/routers/ipliteral.h | 34 + src/routers/iplookup.c | 418 ++ src/routers/iplookup.h | 42 + src/routers/manualroute.c | 490 ++ src/routers/manualroute.h | 39 + src/routers/queryprogram.c | 538 +++ src/routers/queryprogram.h | 40 + src/routers/redirect.c | 801 ++++ src/routers/redirect.h | 70 + src/routers/rf_change_domain.c | 85 + src/routers/rf_expand_data.c | 48 + src/routers/rf_functions.h | 31 + src/routers/rf_get_errors_address.c | 132 + src/routers/rf_get_munge_headers.c | 123 + src/routers/rf_get_transport.c | 97 + src/routers/rf_get_ugid.c | 80 + src/routers/rf_lookup_hostlist.c | 262 ++ src/routers/rf_queue_add.c | 109 + src/routers/rf_self_action.c | 123 + src/routers/rf_set_ugid.c | 44 + src/search.c | 967 ++++ src/setenv.c | 58 + src/sha_ver.h | 47 + src/sieve.c | 3629 ++++++++++++++ src/smtp_in.c | 6068 ++++++++++++++++++++++++ src/smtp_out.c | 936 ++++ src/spam.c | 614 +++ src/spam.h | 40 + src/spf.c | 420 ++ src/spf.h | 38 + src/spool_in.c | 1094 +++++ src/spool_mbox.c | 247 + src/spool_out.c | 580 +++ src/std-crypto.c | 1031 ++++ src/store.c | 1301 ++++++ src/store.h | 89 + src/string.c | 1859 ++++++++ src/structs.h | 960 ++++ src/tls-cipher-stdname.c | 393 ++ src/tls-gnu.c | 4280 +++++++++++++++++ src/tls-openssl.c | 4894 +++++++++++++++++++ src/tls.c | 843 ++++ src/tlscert-gnu.c | 482 ++ src/tlscert-openssl.c | 532 +++ src/tod.c | 239 + src/transport-filter.src | 93 + src/transport.c | 2409 ++++++++++ src/transports/Makefile | 27 + src/transports/README | 41 + src/transports/appendfile.c | 3317 +++++++++++++ src/transports/appendfile.h | 100 + src/transports/autoreply.c | 821 ++++ src/transports/autoreply.h | 45 + src/transports/lmtp.c | 809 ++++ src/transports/lmtp.h | 32 + src/transports/pipe.c | 1124 +++++ src/transports/pipe.h | 51 + src/transports/queuefile.c | 286 ++ src/transports/queuefile.h | 29 + src/transports/smtp.c | 6071 ++++++++++++++++++++++++ src/transports/smtp.h | 252 + src/transports/smtp_socks.c | 415 ++ src/transports/tf_maildir.c | 585 +++ src/transports/tf_maildir.h | 21 + src/tree.c | 393 ++ src/utf8.c | 278 ++ src/valgrind.h | 4797 +++++++++++++++++++ src/verify.c | 3610 ++++++++++++++ src/version.c | 68 + src/version.h | 7 + src/version.sh | 3 + 244 files changed, 187237 insertions(+) create mode 100644 src/EDITME create mode 100644 src/acl.c create mode 100644 src/aliases.default create mode 100644 src/arc.c create mode 100644 src/auths/Makefile create mode 100644 src/auths/README create mode 100644 src/auths/auth-spa.c create mode 100644 src/auths/auth-spa.h create mode 100644 src/auths/call_pam.c create mode 100644 src/auths/call_pwcheck.c create mode 100644 src/auths/call_radius.c create mode 100644 src/auths/check_serv_cond.c create mode 100644 src/auths/cram_md5.c create mode 100644 src/auths/cram_md5.h create mode 100644 src/auths/cyrus_sasl.c create mode 100644 src/auths/cyrus_sasl.h create mode 100644 src/auths/dovecot.c create mode 100644 src/auths/dovecot.h create mode 100644 src/auths/external.c create mode 100644 src/auths/external.h create mode 100644 src/auths/get_data.c create mode 100644 src/auths/get_no64_data.c create mode 100644 src/auths/gsasl_exim.c create mode 100644 src/auths/gsasl_exim.h create mode 100644 src/auths/heimdal_gssapi.c create mode 100644 src/auths/heimdal_gssapi.h create mode 100644 src/auths/plaintext.c create mode 100644 src/auths/plaintext.h create mode 100644 src/auths/pwcheck.c create mode 100644 src/auths/pwcheck.h create mode 100644 src/auths/spa.c create mode 100644 src/auths/spa.h create mode 100644 src/auths/tls.c create mode 100644 src/auths/tls.h create mode 100644 src/auths/xtextdecode.c create mode 100644 src/auths/xtextencode.c create mode 100644 src/base64.c create mode 100644 src/blob.h create mode 100644 src/bmi_spam.c create mode 100644 src/bmi_spam.h create mode 100644 src/buildconfig.c create mode 100644 src/child.c create mode 100644 src/cnumber.h create mode 100644 src/config.h.defaults create mode 100644 src/configure.default create mode 100755 src/convert4r3.src create mode 100755 src/convert4r4.src create mode 100644 src/crypt16.c create mode 100644 src/daemon.c create mode 100644 src/dane-openssl.c create mode 100644 src/dane.c create mode 100644 src/danessl.h create mode 100644 src/dbfn.c create mode 100644 src/dbfunctions.h create mode 100644 src/dcc.c create mode 100644 src/dcc.h create mode 100644 src/debug.c create mode 100644 src/deliver.c create mode 100644 src/directory.c create mode 100644 src/dkim.c create mode 100644 src/dkim.h create mode 100644 src/dkim_transport.c create mode 100644 src/dmarc.c create mode 100644 src/dmarc.h create mode 100644 src/dns.c create mode 100644 src/dnsbl.c create mode 100644 src/drtables.c create mode 100644 src/dummies.c create mode 100644 src/enq.c create mode 100644 src/environment.c create mode 100644 src/exicyclog.src create mode 100644 src/exigrep.src create mode 100644 src/exim.c create mode 100644 src/exim.h create mode 100755 src/exim_checkaccess.src create mode 100644 src/exim_dbmbuild.c create mode 100644 src/exim_dbutil.c create mode 100644 src/exim_lock.c create mode 100644 src/eximon.src create mode 100644 src/eximstats.src create mode 100644 src/exinext.src create mode 100644 src/exipick.src create mode 100644 src/exiqgrep.src create mode 100644 src/exiqsumm.src create mode 100644 src/exiwhat.src create mode 100644 src/expand.c create mode 100644 src/filter.c create mode 100644 src/filtertest.c create mode 100644 src/functions.h create mode 100644 src/globals.c create mode 100644 src/globals.h create mode 100644 src/hash.c create mode 100644 src/hash.h create mode 100644 src/header.c create mode 100644 src/hintsdb.h create mode 100644 src/hintsdb_structs.h create mode 100644 src/host.c create mode 100644 src/imap_utf7.c create mode 100644 src/ip.c create mode 100644 src/local_scan.c create mode 100644 src/local_scan.h create mode 100644 src/log.c create mode 100644 src/lookupapi.h create mode 100644 src/lookups/Makefile create mode 100644 src/lookups/README create mode 100644 src/lookups/cdb.c create mode 100644 src/lookups/dbmdb.c create mode 100644 src/lookups/dnsdb.c create mode 100644 src/lookups/dsearch.c create mode 100644 src/lookups/ibase.c create mode 100644 src/lookups/json.c create mode 100644 src/lookups/ldap.c create mode 100644 src/lookups/ldap.h create mode 100644 src/lookups/lf_check_file.c create mode 100644 src/lookups/lf_functions.h create mode 100644 src/lookups/lf_quote.c create mode 100644 src/lookups/lf_sqlperform.c create mode 100644 src/lookups/lmdb.c create mode 100644 src/lookups/lsearch.c create mode 100644 src/lookups/mysql.c create mode 100644 src/lookups/nis.c create mode 100644 src/lookups/nisplus.c create mode 100644 src/lookups/oracle.c create mode 100644 src/lookups/passwd.c create mode 100644 src/lookups/pgsql.c create mode 100644 src/lookups/readsock.c create mode 100644 src/lookups/redis.c create mode 100644 src/lookups/spf.c create mode 100644 src/lookups/sqlite.c create mode 100644 src/lookups/testdb.c create mode 100644 src/lookups/whoson.c create mode 100644 src/lss.c create mode 100644 src/macro_predef.c create mode 100644 src/macro_predef.h create mode 100644 src/macros.h create mode 100644 src/malware.c create mode 100644 src/match.c create mode 100644 src/md5.c create mode 100644 src/memcheck.h create mode 100644 src/mime.c create mode 100644 src/mime.h create mode 100644 src/moan.c create mode 100644 src/mytypes.h create mode 100644 src/os.c create mode 100644 src/osfunctions.h create mode 100644 src/parse.c create mode 100644 src/pdkim/Makefile create mode 100644 src/pdkim/README create mode 100644 src/pdkim/config.h create mode 100644 src/pdkim/crypt_ver.h create mode 100644 src/pdkim/pdkim.c create mode 100644 src/pdkim/pdkim.h create mode 100644 src/pdkim/pdkim_hash.h create mode 100644 src/pdkim/signing.c create mode 100644 src/pdkim/signing.h create mode 100644 src/perl.c create mode 100644 src/priv.c create mode 100644 src/queue.c create mode 100644 src/rda.c create mode 100644 src/readconf.c create mode 100644 src/receive.c create mode 100644 src/regex.c create mode 100644 src/retry.c create mode 100644 src/rewrite.c create mode 100644 src/rfc2047.c create mode 100644 src/route.c create mode 100644 src/routers/Makefile create mode 100644 src/routers/README create mode 100644 src/routers/accept.c create mode 100644 src/routers/accept.h create mode 100644 src/routers/dnslookup.c create mode 100644 src/routers/dnslookup.h create mode 100644 src/routers/ipliteral.c create mode 100644 src/routers/ipliteral.h create mode 100644 src/routers/iplookup.c create mode 100644 src/routers/iplookup.h create mode 100644 src/routers/manualroute.c create mode 100644 src/routers/manualroute.h create mode 100644 src/routers/queryprogram.c create mode 100644 src/routers/queryprogram.h create mode 100644 src/routers/redirect.c create mode 100644 src/routers/redirect.h create mode 100644 src/routers/rf_change_domain.c create mode 100644 src/routers/rf_expand_data.c create mode 100644 src/routers/rf_functions.h create mode 100644 src/routers/rf_get_errors_address.c create mode 100644 src/routers/rf_get_munge_headers.c create mode 100644 src/routers/rf_get_transport.c create mode 100644 src/routers/rf_get_ugid.c create mode 100644 src/routers/rf_lookup_hostlist.c create mode 100644 src/routers/rf_queue_add.c create mode 100644 src/routers/rf_self_action.c create mode 100644 src/routers/rf_set_ugid.c create mode 100644 src/search.c create mode 100644 src/setenv.c create mode 100644 src/sha_ver.h create mode 100644 src/sieve.c create mode 100644 src/smtp_in.c create mode 100644 src/smtp_out.c create mode 100644 src/spam.c create mode 100644 src/spam.h create mode 100644 src/spf.c create mode 100644 src/spf.h create mode 100644 src/spool_in.c create mode 100644 src/spool_mbox.c create mode 100644 src/spool_out.c create mode 100644 src/std-crypto.c create mode 100644 src/store.c create mode 100644 src/store.h create mode 100644 src/string.c create mode 100644 src/structs.h create mode 100644 src/tls-cipher-stdname.c create mode 100644 src/tls-gnu.c create mode 100644 src/tls-openssl.c create mode 100644 src/tls.c create mode 100644 src/tlscert-gnu.c create mode 100644 src/tlscert-openssl.c create mode 100644 src/tod.c create mode 100644 src/transport-filter.src create mode 100644 src/transport.c create mode 100644 src/transports/Makefile create mode 100644 src/transports/README create mode 100644 src/transports/appendfile.c create mode 100644 src/transports/appendfile.h create mode 100644 src/transports/autoreply.c create mode 100644 src/transports/autoreply.h create mode 100644 src/transports/lmtp.c create mode 100644 src/transports/lmtp.h create mode 100644 src/transports/pipe.c create mode 100644 src/transports/pipe.h create mode 100644 src/transports/queuefile.c create mode 100644 src/transports/queuefile.h create mode 100644 src/transports/smtp.c create mode 100644 src/transports/smtp.h create mode 100644 src/transports/smtp_socks.c create mode 100644 src/transports/tf_maildir.c create mode 100644 src/transports/tf_maildir.h create mode 100644 src/tree.c create mode 100644 src/utf8.c create mode 100644 src/valgrind.h create mode 100644 src/verify.c create mode 100644 src/version.c create mode 100644 src/version.h create mode 100644 src/version.sh (limited to 'src') diff --git a/src/EDITME b/src/EDITME new file mode 100644 index 0000000..53022e5 --- /dev/null +++ b/src/EDITME @@ -0,0 +1,1497 @@ +################################################## +# The Exim mail transport agent # +################################################## + +# This is the template for Exim's main build-time configuration file. It +# contains settings that are independent of any operating system. These are +# things that are mostly sysadmin choices. The items below are divided into +# those you must specify, those you probably want to specify, those you might +# often want to specify, and those that you almost never need to mention. + +# Edit this file and save the result to a file called Local/Makefile within the +# Exim distribution directory before running the "make" command. + +# Things that depend on the operating system have default settings in +# OS/Makefile-Default, but these are overridden for some OS by files +# called OS/Makefile-. You can further override these settings by +# creating files Local/Makefile-, and Local/Makefile-. +# The suffix "" stands for the name of your operating system - look +# at the names in the OS directory to see which names are recognized, +# and "" is the content of the environment variable "build". + +# However, if you are building Exim for a single OS only, you don't need to +# worry about setting up Local/Makefile-. Any build-time configuration +# settings you require can in fact be placed in the one file called +# Local/Makefile. It is only if you are building for several OS from the same +# source files that you need to worry about splitting off your own OS-dependent +# settings into separate files. (There's more explanation about how this all +# works in the toplevel README file, under "Modifying the building process", as +# well as in the Exim specification.) + +# One OS-specific thing that may need to be changed is the command for running +# the C compiler; the overall default is gcc, but some OS Makefiles specify cc. +# You can override anything that is set by putting CC=whatever in your +# Local/Makefile. + +# NOTE: You should never need to edit any of the distributed Makefiles; all +# overriding can be done in your Local/Makefile(s). This will make it easier +# for you when the next release comes along. + +# The location of the X11 libraries is something else that is quite variable +# even between different versions of the same operating system (and indeed +# there are different versions of X11 as well, of course). The four settings +# concerned here are X11, XINCLUDE, XLFLAGS (linking flags) and X11_LD_LIB +# (dynamic run-time library). You need not worry about X11 unless you want to +# compile the Exim monitor utility. Exim itself does not use X11. + +# Another area of variability between systems is the type and location of the +# DBM library package. Exim has support for ndbm, gdbm, tdb, and Berkeley DB. +# By default the code assumes ndbm; this often works with gdbm or DB, provided +# they are correctly installed, via their compatibility interfaces. However, +# Exim can also be configured to use the native calls for Berkeley DB (obsolete +# versions 1.85, 2.x, 3.x, or the current 4.x version) and also for gdbm. + +# For some operating systems, a default DBM library (other than ndbm) is +# selected by a setting in the OS-specific Makefile. Most modern OS now have +# a DBM library installed as standard, and in many cases this will be selected +# for you by the OS-specific configuration. If Exim compiles without any +# problems, you probably do not have to worry about the DBM library. If you +# do want or need to change it, you should first read the discussion in the +# file doc/dbm.discuss.txt, which also contains instructions for testing Exim's +# interface to the DBM library. + +# In Local/Makefiles blank lines and lines starting with # are ignored. It is +# also permitted to use the # character to add a comment to a setting, for +# example +# +# EXIM_GID=42 # the "mail" group +# +# However, with some versions of "make" this works only if there is no white +# space between the end of the setting and the #, so perhaps it is best +# avoided. A consequence of this facility is that it is not possible to have +# the # character present in any setting, but I can't think of any cases where +# this would be wanted. +############################################################################### + + + +############################################################################### +# THESE ARE THINGS YOU MUST SPECIFY # +############################################################################### + +# Exim will not build unless you specify BIN_DIRECTORY, CONFIGURE_FILE, and +# EXIM_USER. You also need EXIM_GROUP if EXIM_USER specifies a uid by number. + +# If you don't specify SPOOL_DIRECTORY, Exim won't fail to build. However, it +# really is a very good idea to specify it here rather than at run time. This +# is particularly true if you let the logs go to their default location in the +# spool directory, because it means that the location of the logs is known +# before Exim has read the run time configuration file. + +#------------------------------------------------------------------------------ +# BIN_DIRECTORY defines where the exim binary will be installed by "make +# install". The path is also used internally by Exim when it needs to re-invoke +# itself, either to send an error message, or to recover root privilege. Exim's +# utility binaries and scripts are also installed in this directory. There is +# no "standard" place for the binary directory. Some people like to keep all +# the Exim files under one directory such as /usr/exim; others just let the +# Exim binaries go into an existing directory such as /usr/sbin or +# /usr/local/sbin. The installation script will try to create this directory, +# and any superior directories, if they do not exist. + +BIN_DIRECTORY=/usr/exim/bin + + +#------------------------------------------------------------------------------ +# CONFIGURE_FILE defines where Exim's run time configuration file is to be +# found. It is the complete pathname for the file, not just a directory. The +# location of all other run time files and directories can be changed in the +# run time configuration file. There is a lot of variety in the choice of +# location in different OS, and in the preferences of different sysadmins. Some +# common locations are in /etc or /etc/mail or /usr/local/etc or +# /usr/local/etc/mail. Another possibility is to keep all the Exim files under +# a single directory such as /usr/exim. Whatever you choose, the installation +# script will try to make the directory and any superior directories if they +# don't exist. It will also install a default runtime configuration if this +# file does not exist. + +CONFIGURE_FILE=/usr/exim/configure + +# It is possible to specify a colon-separated list of files for CONFIGURE_FILE. +# In this case, Exim will use the first of them that exists when it is run. +# However, if a list is specified, the installation script no longer tries to +# make superior directories or to install a default runtime configuration. + + +#------------------------------------------------------------------------------ +# The Exim binary must normally be setuid root, so that it starts executing as +# root, but (depending on the options with which it is called) it does not +# always need to retain the root privilege. These settings define the user and +# group that is used for Exim processes when they no longer need to be root. In +# particular, this applies when receiving messages and when doing remote +# deliveries. (Local deliveries run as various non-root users, typically as the +# owner of a local mailbox.) Specifying these values as root is not supported. + +EXIM_USER= + +# If you specify EXIM_USER as a name, this is looked up at build time, and the +# uid number is built into the binary. However, you can specify that this +# lookup is deferred until runtime. In this case, it is the name that is built +# into the binary. You can do this by a setting of the form: + +# EXIM_USER=ref:exim + +# In other words, put "ref:" in front of the user name. If you set EXIM_USER +# like this, any value specified for EXIM_GROUP is also passed "by reference". +# Although this costs a bit of resource at runtime, it is convenient to use +# this feature when building binaries that are to be run on multiple systems +# where the name may refer to different uids. It also allows you to build Exim +# on a system where there is no Exim user defined. + +# If the setting of EXIM_USER is numeric (e.g. EXIM_USER=42), there must +# also be a setting of EXIM_GROUP. If, on the other hand, you use a name +# for EXIM_USER (e.g. EXIM_USER=exim), you don't need to set EXIM_GROUP unless +# you want to use a group other than the default group for the given user. + +# EXIM_GROUP= + +# Many sites define a user called "exim", with an appropriate default group, +# and use +# +# EXIM_USER=exim +# +# while leaving EXIM_GROUP unspecified (commented out). + + +#------------------------------------------------------------------------------ +# SPOOL_DIRECTORY defines the directory where all the data for messages in +# transit is kept. It is strongly recommended that you define it here, though +# it is possible to leave this till the run time configuration. + +# Exim creates the spool directory if it does not exist. The owner and group +# will be those defined by EXIM_USER and EXIM_GROUP, and this also applies to +# all the files and directories that are created in the spool directory. + +# Almost all installations choose this: + +SPOOL_DIRECTORY=/var/spool/exim + + + +############################################################################### +# TLS # +############################################################################### +# Exim is built by default to support the SMTP STARTTLS command, which implements +# Transport Layer Security using SSL (Secure Sockets Layer). This requires you +# must install the OpenSSL library package or the GnuTLS library. Exim contains +# no cryptographic code of its own. + +# If you are running Exim as a (TLS) server, just building it with TLS support +# is all you need to do, as tls_advertise_hosts is set to '*' by +# default. But you are advised to create a suiteable certificate, and tell +# Exim about it by means of the tls_certificate and tls_privatekey run +# time options, otherwise Exim will create a self signed certificate on +# the fly. If you are running Exim only as a (TLS) client, building it with +# TLS support is all you need to do. +# +# If you are using pkg-config then you should not need to worry where +# the libraries and headers are installed, as the pkg-config .pc +# specification should include all -L/-I information necessary. +# Enabling the USE_*_PC options should be sufficient. If not using +# pkg-config, then you have to specify the libraries, and you might +# need to specify the locations too. + +# Uncomment the following lines if you want +# to build Exim without any TLS support (either OpenSSL or GnuTLS): +# DISABLE_TLS=yes +# Unless you do this, you must define one of USE_OPENSSL or USE_GNUTLS +# below. + +# If you are building with TLS, the library configuration must be done: + +# Uncomment this if you are using OpenSSL +# USE_OPENSSL=yes +# Uncomment one of these settings if you are using OpenSSL; pkg-config vs not +# and an optional location. +# USE_OPENSSL_PC=openssl +# TLS_LIBS=-lssl -lcrypto +# TLS_LIBS=-L/usr/local/openssl/lib -lssl -lcrypto + +# Uncomment this if you are using GnuTLS +# USE_GNUTLS=yes +# Uncomment one of these settings if you are using GnuTLS; pkg-config vs not +# and an optional location. If you disable SUPPORT_DANE below, you +# can remove the gnutls-dane references here. Earlier versions of GnuTLS +# required libtasn1 and libgrypt also; add if needed. +# USE_GNUTLS_PC=gnutls gnutls-dane +# TLS_LIBS=-lgnutls -lgnutls-dane +# TLS_LIBS=-L/usr/local/gnu/lib -lgnutls -ltasn1 -lgcrypt -lgnutls-dane + +# If using GnuTLS older than 2.10 and using pkg-config then note that Exim's +# build process will require libgcrypt-config to exist in your $PATH. A +# version that old is likely to become unsupported by Exim in 2017. + +# The security fix we provide with the gnutls_allow_auto_pkcs11 option +# (4.82 PP/09) introduces a compatibility regression. The symbol is +# not available if GnuTLS is build without p11-kit (--without-p11-kit +# configure option). In this case use AVOID_GNUTLS_PKCS11=yes when +# building Exim. +# AVOID_GNUTLS_PKCS11=yes + +# If you are running Exim as a server, note that just building it with TLS +# support is not all you need to do. You also need to set up a suitable +# certificate, and tell Exim about it by means of the tls_certificate +# and tls_privatekey run time options. You also need to set tls_advertise_hosts +# to specify the hosts to which Exim advertises TLS support. On the other hand, +# if you are running Exim only as a client, building it with TLS support +# is all you need to do. + +# If you are using pkg-config then you should not need to worry where the +# libraries and headers are installed, as the pkg-config .pc specification +# should include all -L/-I information necessary. If not using pkg-config +# then you might need to specify the locations too. + +# Additional libraries and include files are required for both OpenSSL and +# GnuTLS. The TLS_LIBS settings above assume that the libraries are installed +# with all your other libraries. If they are in a special directory, you may +# need something like + +# TLS_LIBS=-L/usr/local/openssl/lib -lssl -lcrypto + +# or + +# TLS_LIBS=-L/opt/gnu/lib -lgnutls -ltasn1 -lgcrypt -lgnutls-dane +# If not using DANE under GnuTLS we can lose one library +# TLS_LIBS=-L/opt/gnu/lib -lgnutls -ltasn1 -lgcrypt + +# TLS_LIBS is included only on the command for linking Exim itself, not on any +# auxiliary programs. If the include files are not in a standard place, you can +# set TLS_INCLUDE to specify where they are, for example: + +# TLS_INCLUDE=-I/usr/local/openssl/include/ +# or +# TLS_INCLUDE=-I/opt/gnu/include + +# You don't need to set TLS_INCLUDE if the relevant directories are already +# specified in INCLUDE. + + +# Uncomment the following line to remove support for TLS Resumption +# DISABLE_TLS_RESUME=yes + + +############################################################################### +# THESE ARE THINGS YOU PROBABLY WANT TO SPECIFY # +############################################################################### + +# If you need extra header file search paths on all compiles, put the -I +# options in INCLUDE. If you want the extra searches only for certain +# parts of the build, see more specific xxx_INCLUDE variables below. + +# INCLUDE=-I/example/include + +# You need to specify some routers and transports if you want the Exim that you +# are building to be capable of delivering mail. You almost certainly need at +# least one type of lookup. You should consider whether you want to build +# the Exim monitor or not. + +# If you need to override how pkg-config finds configuration files for +# installed software, then you can set that here; wildcards will be expanded. + +# PKG_CONFIG_PATH=/usr/local/opt/openssl/lib/pkgconfig : /opt/*/lib/pkgconfig + + +#------------------------------------------------------------------------------ +# These settings determine which individual router drivers are included in the +# Exim binary. There are no defaults in the code; those routers that are wanted +# must be defined here by setting the appropriate variables to the value "yes". +# Including a router in the binary does not cause it to be used automatically. +# It has also to be configured in the run time configuration file. By +# commenting out those you know you don't want to use, you can make the binary +# a bit smaller. If you are unsure, leave all of these included for now. + +ROUTER_ACCEPT=yes +ROUTER_DNSLOOKUP=yes +ROUTER_IPLITERAL=yes +ROUTER_MANUALROUTE=yes +ROUTER_QUERYPROGRAM=yes +ROUTER_REDIRECT=yes + +# This one is very special-purpose, so is not included by default. + +# ROUTER_IPLOOKUP=yes + + +#------------------------------------------------------------------------------ +# These settings determine which individual transport drivers are included in +# the Exim binary. There are no defaults; those transports that are wanted must +# be defined here by setting the appropriate variables to the value "yes". +# Including a transport in the binary does not cause it to be used +# automatically. It has also to be configured in the run time configuration +# file. By commenting out those you know you don't want to use, you can make +# the binary a bit smaller. If you are unsure, leave all of these included for +# now. + +TRANSPORT_APPENDFILE=yes +TRANSPORT_AUTOREPLY=yes +TRANSPORT_PIPE=yes +TRANSPORT_SMTP=yes + +# This one is special-purpose, and commonly not required, so it is not +# included by default. + +# TRANSPORT_LMTP=yes + + +#------------------------------------------------------------------------------ +# The appendfile transport can write messages to local mailboxes in a number +# of formats. The code for three specialist formats, maildir, mailstore, and +# MBX, is included only when requested. If you do not know what this is about, +# leave these settings commented out. + +# SUPPORT_MAILDIR=yes +# SUPPORT_MAILSTORE=yes +# SUPPORT_MBX=yes + + +#------------------------------------------------------------------------------ +# See below for dynamic lookup modules. +# +# If not using package management but using this anyway, then think about how +# you perform upgrades and revert them. You should consider the benefit of +# embedding the Exim version number into LOOKUP_MODULE_DIR, so that you can +# maintain two concurrent sets of modules. +# +# *BEWARE*: ability to modify the files in LOOKUP_MODULE_DIR is equivalent to +# the ability to modify the Exim binary, which is often setuid root! The Exim +# developers only intend this functionality be used by OS software packagers +# and we suggest that such packagings' integrity checks should be paranoid +# about the permissions of the directory and the files within. + +# LOOKUP_MODULE_DIR=/usr/lib/exim/lookups/ + +# To build a module dynamically, you'll need to define CFLAGS_DYNAMIC for +# your platform. Eg: +# CFLAGS_DYNAMIC=-shared -rdynamic +# CFLAGS_DYNAMIC=-shared -rdynamic -fPIC + +#------------------------------------------------------------------------------ +# These settings determine which file and database lookup methods are included +# in the binary. See the manual chapter entitled "File and database lookups" +# for discussion. DBM and lsearch (linear search) are included by default. If +# you are unsure about the others, leave them commented out for now. +# LOOKUP_DNSDB does *not* refer to general mail routing using the DNS. It is +# for the specialist case of using the DNS as a general database facility (not +# common). +# If set to "2" instead of "yes" then the corresponding lookup will be +# built as a module and must be installed into LOOKUP_MODULE_DIR. You need to +# add -export-dynamic -rdynamic to EXTRALIBS. You may also need to add -ldl to +# EXTRALIBS so that dlopen() is available to Exim. You need to define +# LOOKUP_MODULE_DIR above so the exim binary actually loads dynamic lookup +# modules. +# Also, instead of adding all the libraries/includes to LOOKUP_INCLUDE and +# LOOKUP_LIBS, add them to the respective LOOKUP_*_INCLUDE and LOOKUP_*_LIBS +# (where * is the name as given here in this list). That ensures that only +# the dynamic library and not the exim binary will be linked against the +# library. +# NOTE: LDAP cannot be built as a module! +# +# For Redis you need to have hiredis installed on your system +# (https://github.com/redis/hiredis). +# Depending on where it is installed you may have to edit the CFLAGS +# (often += -I/usr/local/include) and LDFLAGS (-lhiredis) lines. + +# If your system has pkg-config then the _INCLUDE/_LIBS setting can be +# handled for you automatically by also defining the _PC variable to reference +# the name of the pkg-config package, if such is available. + +LOOKUP_DBM=yes +LOOKUP_LSEARCH=yes +LOOKUP_DNSDB=yes + +# LOOKUP_CDB=yes +# LOOKUP_DSEARCH=yes +# LOOKUP_IBASE=yes +# LOOKUP_JSON=yes +# LOOKUP_LDAP=yes +# LOOKUP_LMDB=yes + +# LOOKUP_MYSQL=yes +# LOOKUP_MYSQL_PC=mariadb +# LOOKUP_NIS=yes +# LOOKUP_NISPLUS=yes +# LOOKUP_ORACLE=yes +# LOOKUP_PASSWD=yes +# LOOKUP_PGSQL=yes +# LOOKUP_REDIS=yes +# LOOKUP_SQLITE=yes +# LOOKUP_SQLITE_PC=sqlite3 +# LOOKUP_WHOSON=yes + +# These two settings are obsolete; all three lookups are compiled when +# LOOKUP_LSEARCH is enabled. However, we retain these for backward +# compatibility. Setting one forces LOOKUP_LSEARCH if it is not set. + +# LOOKUP_WILDLSEARCH=yes +# LOOKUP_NWILDLSEARCH=yes + + +# Some platforms may need this for LOOKUP_NIS: +# LIBS += -lnsl + +#------------------------------------------------------------------------------ +# If you have set LOOKUP_LDAP=yes, you should set LDAP_LIB_TYPE to indicate +# which LDAP library you have. Unfortunately, though most of their functions +# are the same, there are minor differences. Currently Exim knows about four +# LDAP libraries: the one from the University of Michigan (also known as +# OpenLDAP 1), OpenLDAP 2, the Netscape SDK library, and the library that comes +# with Solaris 7 onwards. Uncomment whichever of these you are using. + +# LDAP_LIB_TYPE=OPENLDAP1 +# LDAP_LIB_TYPE=OPENLDAP2 +# LDAP_LIB_TYPE=NETSCAPE +# LDAP_LIB_TYPE=SOLARIS + +# If you don't set any of these, Exim assumes the original University of +# Michigan (OpenLDAP 1) library. + + +#------------------------------------------------------------------------------ +# The PCRE2 library is required for Exim. There is no longer an embedded +# version of the PCRE library included with the source code, instead you +# must use a system library or build your own copy of PCRE2. +# In either case you must specify the library link info here. If the +# PCRE2 header files are not in the standard search path you must also +# modify the INCLUDE path (above) +# +# Use PCRE_CONFIG to query the pcre-config command (first found in $PATH) +# to find the include files and libraries, else use PCRE_LIBS and set INCLUDE +# too if needed. + +PCRE2_CONFIG=yes +# PCRE_LIBS=-lpcre2 + + +#------------------------------------------------------------------------------ +# Comment out the following line to remove DANE support +# Note: Enabling this unconditionally overrides DISABLE_DNSSEC +# forces you to have SUPPORT_TLS enabled (the default). For DANE under +# GnuTLS we need an additional library. See TLS_LIBS or USE_GNUTLS_PC +# below. +SUPPORT_DANE=yes + +#------------------------------------------------------------------------------ +# Additional libraries and include directories may be required for some +# lookup styles (e.g. LDAP, MYSQL or PGSQL). LOOKUP_LIBS is included only on +# the command for linking Exim itself, not on any auxiliary programs. You +# don't need to set LOOKUP_INCLUDE if the relevant directories are already +# specified in INCLUDE. The settings below are just examples; -lpq is for +# PostgreSQL, -lgds is for Interbase, -lsqlite3 is for SQLite, -lhiredis +# is for Redis, -ljansson for JSON. +# +# You do not need to use this for any lookup information added via pkg-config. + +# LOOKUP_INCLUDE=-I /usr/local/ldap/include -I /usr/local/mysql/include -I /usr/local/pgsql/include +# LOOKUP_INCLUDE +=-I /usr/local/include +# LOOKUP_LIBS=-L/usr/local/lib -lldap -llber -lmysqlclient -lpq -lgds -lsqlite3 -llmdb + +#------------------------------------------------------------------------------ +# If you included LOOKUP_LMDB above you will need the library. Depending +# on where installed you may also need an include directory +# +# LOOKUP_INCLUDE += -I/usr/local/include +# LOOKUP_LIBS += -llmdb + + +#------------------------------------------------------------------------------ +# Compiling the Exim monitor: If you want to compile the Exim monitor, a +# program that requires an X11 display, then EXIM_MONITOR should be set to the +# value "eximon.bin". De-comment this setting to enable compilation of the +# monitor. The locations of various X11 directories for libraries and include +# files are defaulted in the OS/Makefile-Default file, but can be overridden in +# local OS-specific make files. + +# EXIM_MONITOR=eximon.bin + + +#------------------------------------------------------------------------------ +# Compiling Exim with content scanning support: If you want to compile Exim +# with support for message body content scanning, set WITH_CONTENT_SCAN to +# the value "yes". This will give you malware and spam scanning in the DATA ACL, +# and the MIME ACL. Please read the documentation to learn more about these +# features. + +# WITH_CONTENT_SCAN=yes + +# If you have content scanning you may wish to only include some of the scanner +# interfaces. Uncomment any of these lines to remove that code. + +# DISABLE_MAL_FFROTD=yes +# DISABLE_MAL_FFROT6D=yes +# DISABLE_MAL_DRWEB=yes +# DISABLE_MAL_FSECURE=yes +# DISABLE_MAL_SOPHIE=yes +# DISABLE_MAL_CLAM=yes +# DISABLE_MAL_AVAST=yes +# DISABLE_MAL_SOCK=yes +# DISABLE_MAL_CMDLINE=yes + +# These scanners are claimed to be no longer existent. + +DISABLE_MAL_AVE=yes +DISABLE_MAL_KAV=yes +DISABLE_MAL_MKS=yes + + +#------------------------------------------------------------------------------ +# If built with TLS, Exim includes code to support DKIM (DomainKeys Identified +# Mail, RFC4871) signing and verification. Verification of signatures is +# turned on by default. See the spec for information on conditionally +# disabling it. To disable the inclusion of the entire feature, set +# DISABLE_DKIM to "yes" + +# DISABLE_DKIM=yes + +#------------------------------------------------------------------------------ +# Uncomment the following line to remove Per-Recipient-Data-Response support. + +# DISABLE_PRDR=yes + +#------------------------------------------------------------------------------ +# Uncomment the following line to remove OCSP stapling support in TLS, +# from Exim. Note it can only be supported when built with +# GnuTLS 3.1.3 or later, or OpenSSL + +# DISABLE_OCSP=yes + +#------------------------------------------------------------------------------ +# By default, Exim has support for checking the AD bit in a DNS response, to +# determine if DNSSEC validation was successful. If your system libraries +# do not support that bit, then set DISABLE_DNSSEC to "yes" +# Note: Enabling SUPPORT_DANE unconditionally overrides this setting. + +# DISABLE_DNSSEC=yes + +# To disable support for Events set DISABLE_EVENT to "yes" +# DISABLE_EVENT=yes + + +# Uncomment this line to remove support for early pipelining, per +# https://datatracker.ietf.org/doc/draft-harris-early-pipe/ +# DISABLE_PIPE_CONNECT=yes + + +# Uncomment the following to remove the fast-ramp two-phase-queue-run support +# DISABLE_QUEUE_RAMP=yes + +# Uncomment the following lines to add SRS (Sender Rewriting Scheme) support +# using only native facilities. +# SUPPORT_SRS=yes + + +#------------------------------------------------------------------------------ +# Compiling Exim with experimental features. These are documented in +# experimental-spec.txt. "Experimental" means that the way these features are +# implemented may still change. Backward compatibility is not guaranteed. + +# Uncomment the following line to add support for talking to dccifd. This +# defaults the socket path to /usr/local/dcc/var/dccifd. +# Doing so will also explicitly turn on the WITH_CONTENT_SCAN option. + +# EXPERIMENTAL_DCC=yes + +# Uncomment the following line to add DMARC checking capability, implemented +# using libopendmarc libraries. You must have SPF and DKIM support enabled also. +# Library version libopendmarc-1.4.1-1.fc33.x86_64 (on Fedora 33) is known broken; +# 1.3.2-3 works. I seems that the OpenDMARC project broke their API. +# SUPPORT_DMARC=yes +# CFLAGS += -I/usr/local/include +# LDFLAGS += -lopendmarc +# Uncomment the following if you need to change the default. You can +# override it at runtime (main config option dmarc_tld_file) +# DMARC_TLD_FILE=/etc/exim/opendmarc.tlds + +# Uncomment the following line to add ARC (Authenticated Received Chain) +# support. You must have SPF and DKIM support enabled also. +# EXPERIMENTAL_ARC=yes + +# Uncomment the following lines to add Brightmail AntiSpam support. You need +# to have the Brightmail client SDK installed. Please check the experimental +# documentation for implementation details. You need to edit the CFLAGS and +# LDFLAGS lines. + +# EXPERIMENTAL_BRIGHTMAIL=yes +# CFLAGS += -I/opt/brightmail/bsdk-6.0/include +# LDFLAGS += -lxml2_single -lbmiclient_single -L/opt/brightmail/bsdk-6.0/lib + +# Uncomment the following to include extra information in fail DSN message (bounces) +# EXPERIMENTAL_DSN_INFO=yes + +# Uncomment the following line to add queuefile transport support +# EXPERIMENTAL_QUEUEFILE=yes + +############################################################################### +# THESE ARE THINGS YOU MIGHT WANT TO SPECIFY # +############################################################################### + +# The items in this section are those that are commonly changed according to +# the sysadmin's preferences, but whose defaults are often acceptable. The +# first five are concerned with security issues, where differing levels of +# paranoia are appropriate in different environments. Sysadmins also vary in +# their views on appropriate levels of defence in these areas. If you do not +# understand these issues, go with the defaults, which are used by many sites. + + +#------------------------------------------------------------------------------ +# Although Exim is normally a setuid program, owned by root, it refuses to run +# local deliveries as root by default. There is a runtime option called +# "never_users" which lists the users that must never be used for local +# deliveries. There is also the setting below, which provides a list that +# cannot be overridden at runtime. This guards against problems caused by +# unauthorized changes to the runtime configuration. You are advised not to +# remove "root" from this option, but you can add other users if you want. The +# list is colon-separated. It must NOT contain any spaces. + +# FIXED_NEVER_USERS=root:bin:daemon +FIXED_NEVER_USERS=root + + +#------------------------------------------------------------------------------ +# By default, Exim insists that its configuration file be owned by root. You +# can specify one additional permitted owner here. + +# CONFIGURE_OWNER= + +# If the configuration file is group-writeable, Exim insists by default that it +# is owned by root. You can specify one additional permitted group owner here. + +# CONFIGURE_GROUP= + +# If you specify CONFIGURE_OWNER or CONFIGURE_GROUP as a name, this is looked +# up at build time, and the uid or gid number is built into the binary. +# However, you can specify that the lookup is deferred until runtime. In this +# case, it is the name that is built into the binary. You can do this by a +# setting of the form: + +# CONFIGURE_OWNER=ref:mail +# CONFIGURE_GROUP=ref:sysadmin + +# In other words, put "ref:" in front of the user or group name. Although this +# costs a bit of resource at runtime, it is convenient to use this feature when +# building binaries that are to be run on multiple systems where the names may +# refer to different uids or gids. It also allows you to build Exim on a system +# where the relevant user or group is not defined. + + +#------------------------------------------------------------------------------ +# The -C option allows Exim to be run with an alternate runtime configuration +# file. When this is used by root, root privilege is retained by the binary +# (for any other caller including the Exim user, it is dropped). You can +# restrict the location of alternate configurations by defining a prefix below. +# Any file used with -C must then start with this prefix (except that /dev/null +# is also permitted if the caller is root, because that is used in the install +# script). If the prefix specifies a directory that is owned by root, a +# compromise of the Exim account does not permit arbitrary alternate +# configurations to be used. The prefix can be more restrictive than just a +# directory (the second example). + +# ALT_CONFIG_PREFIX=/some/directory/ +# ALT_CONFIG_PREFIX=/some/directory/exim.conf- + + +#------------------------------------------------------------------------------ +# When a user other than root uses the -C option to override the configuration +# file (including the Exim user when re-executing Exim to regain root +# privileges for local message delivery), this will normally cause Exim to +# drop root privileges. The TRUSTED_CONFIG_LIST option, specifies a file which +# contains a list of trusted configuration filenames, one per line. If the -C +# option is used by the Exim user or by the user specified in the +# CONFIGURE_OWNER setting, to specify a configuration file which is listed in +# the TRUSTED_CONFIG_LIST file, then root privileges are not dropped by Exim. + +# TRUSTED_CONFIG_LIST=/usr/exim/trusted_configs + + +#------------------------------------------------------------------------------ +# Uncommenting this option disables the use of the -D command line option, +# which changes the values of macros in the runtime configuration file. +# This is another protection against somebody breaking into the Exim account. + +# DISABLE_D_OPTION=yes + + +#------------------------------------------------------------------------------ +# By contrast, you might be maintaining a system which relies upon the ability +# to override values with -D and assumes that these will be passed through to +# the delivery processes. As of Exim 4.73, this is no longer the case by +# default. Going forward, we strongly recommend that you use a shim Exim +# configuration file owned by root stored under TRUSTED_CONFIG_LIST. +# That shim can set macros before .include'ing your main configuration file. +# +# As a strictly transient measure to ease migration to 4.73, the +# WHITELIST_D_MACROS value defines a colon-separated list of macro-names +# which are permitted to be overridden from the command-line which will be +# honoured by the Exim user. So these are macros that can persist to delivery +# time. +# Examples might be -DTLS or -DSPOOL=/some/dir. The values on the +# command-line are filtered to only permit: [A-Za-z0-9_/.-]* +# +# This option is highly likely to be removed in a future release. It exists +# only to make 4.73 as easy as possible to migrate to. If you use it, we +# encourage you to schedule time to rework your configuration to not depend +# upon it. Most people should not need to use this. +# +# By default, no macros are whitelisted for -D usage. + +# WHITELIST_D_MACROS=TLS:SPOOL + +#------------------------------------------------------------------------------ +# Exim has support for the AUTH (authentication) extension of the SMTP +# protocol, as defined by RFC 2554. If you don't know what SMTP authentication +# is, you probably won't want to include this code, so you should leave these +# settings commented out. If you do want to make use of SMTP authentication, +# you must uncomment at least one of the following, so that appropriate code is +# included in the Exim binary. You will then need to set up the run time +# configuration to make use of the mechanism(s) selected. + +# AUTH_CRAM_MD5=yes +# AUTH_CYRUS_SASL=yes +# AUTH_DOVECOT=yes +# AUTH_EXTERNAL=yes +# AUTH_GSASL=yes +# AUTH_GSASL_PC=libgsasl +# AUTH_HEIMDAL_GSSAPI=yes +# AUTH_HEIMDAL_GSSAPI_PC=heimdal-gssapi +# AUTH_HEIMDAL_GSSAPI_PC=heimdal-gssapi heimdal-krb5 +# AUTH_PLAINTEXT=yes +# AUTH_SPA=yes +# AUTH_TLS=yes + +# Heimdal through 1.5 required pkg-config 'heimdal-gssapi'; Heimdal 7.1 +# requires multiple pkg-config files to work with Exim, so the second example +# above is needed. + +#------------------------------------------------------------------------------ +# If you specified AUTH_CYRUS_SASL above, you should ensure that you have the +# Cyrus SASL library installed before trying to build Exim, and you probably +# want to uncomment the first line below. +# Similarly for GNU SASL, unless pkg-config is used via AUTH_GSASL_PC. +# Ditto for AUTH_HEIMDAL_GSSAPI(_PC). + +# AUTH_LIBS=-lsasl2 +# AUTH_LIBS=-lgsasl +# AUTH_LIBS=-lgssapi -lheimntlm -lkrb5 -lhx509 -lcom_err -lhcrypto -lasn1 -lwind -lroken -lcrypt + +# If using AUTH_GSASL with SCRAM methods, you should also be defining +# SUPPORT_I18N to get standards-conformant support of utf8 normalization. + + +#------------------------------------------------------------------------------ +# When Exim is decoding MIME "words" in header lines, most commonly for use +# in the $header_xxx expansion, it converts any foreign character sets to the +# one that is set in the headers_charset option. The default setting is +# defined by this setting: + +HEADERS_CHARSET="ISO-8859-1" + +# If you are going to make use of $header_xxx expansions in your configuration +# file, or if your users are going to use them in filter files, and the normal +# character set on your host is something other than ISO-8859-1, you might +# like to specify a different default here. This value can be overridden in +# the runtime configuration, and it can also be overridden in individual filter +# files. +# +# IMPORTANT NOTE: The iconv() function is needed for character code +# conversions. Please see the next item... + + +#------------------------------------------------------------------------------ +# Character code conversions are possible only if the iconv() function is +# installed on your operating system. There are two places in Exim where this +# is relevant: (a) The $header_xxx expansion (see the previous item), and (b) +# the Sieve filter support. For those OS where iconv() is known to be installed +# as standard, the file in OS/Makefile-xxxx contains +# +# HAVE_ICONV=yes +# +# If you are not using one of those systems, but have installed iconv(), you +# need to uncomment that line above. In some cases, you may find that iconv() +# and its header file are not in the default places. You might need to use +# something like this: +# +# HAVE_ICONV=yes +# CFLAGS=-O -I/usr/local/include +# EXTRALIBS_EXIM=-L/usr/local/lib -liconv +# +# but of course there may need to be other things in CFLAGS and EXTRALIBS_EXIM +# as well. +# +# nb: FreeBSD as of 4.89 defines LIBICONV_PLUG to pick up the system iconv +# more reliably. If you explicitly want the libiconv Port then as well +# as adding -liconv you'll want to unset LIBICONV_PLUG. If you actually need +# this, let us know, but for now the Exim Maintainers are assuming that this +# is uncommon and so you'll need to edit OS/os.h-FreeBSD yourself to remove +# the define. + + +#------------------------------------------------------------------------------ +# The passwords for user accounts are normally encrypted with the crypt() +# function. Comparisons with encrypted passwords can be done using Exim's +# "crypteq" expansion operator. (This is commonly used as part of the +# configuration of an authenticator for use with SMTP AUTH.) At least one +# operating system has an extended function called crypt16(), which uses up to +# 16 characters of a password (the normal crypt() uses only the first 8). Exim +# supports the use of crypt16() as well as crypt() but note the warning below. + +# You can always indicate a crypt16-encrypted password by preceding it with +# "{crypt16}". If you want the default handling (without any preceding +# indicator) to use crypt16(), uncomment the following line: + +# DEFAULT_CRYPT=crypt16 + +# If you do that, you can still access the basic crypt() function by preceding +# an encrypted password with "{crypt}". For more details, see the description +# of the "crypteq" condition in the manual chapter on string expansions. + +# Some operating systems do not include a crypt16() function, so Exim has one +# of its own, which it uses unless HAVE_CRYPT16 is defined. Normally, that will +# be set in an OS-specific Makefile for the OS that have such a function, so +# you should not need to bother with it. + +# *** WARNING *** WARNING *** WARNING *** WARNING *** WARNING *** +# It turns out that the above is not entirely accurate. As well as crypt16() +# there is a function called bigcrypt() that some operating systems have. This +# may or may not use the same algorithm, and both of them may be different to +# Exim's built-in crypt16() that is used unless HAVE_CRYPT16 is defined. +# +# However, since there is now a move away from the traditional crypt() +# functions towards using SHA1 and other algorithms, tidying up this area of +# Exim is seen as very low priority. In practice, if you need to, you can +# define DEFAULT_CRYPT to the name of any function that has the same interface +# as the traditional crypt() function. +# *** WARNING *** WARNING *** WARNING *** WARNING *** WARNING *** + + +#------------------------------------------------------------------------------ +# The default distribution of Exim contains only the plain text form of the +# documentation. Other forms are available separately. If you want to install +# the documentation in "info" format, first fetch the Texinfo documentation +# sources from the ftp directory and unpack them, which should create files +# with the extension "texinfo" in the doc directory. You may find that the +# version number of the texinfo files is different to your Exim version number, +# because the main documentation isn't updated as often as the code. For +# example, if you have Exim version 4.43, the source tarball unpacks into a +# directory called exim-4.43, but the texinfo tarball unpacks into exim-4.40. +# In this case, move the contents of exim-4.40/doc into exim-4.43/doc after you +# have unpacked them. Then set INFO_DIRECTORY to the location of your info +# directory. This varies from system to system, but is often /usr/share/info. +# Once you have done this, "make install" will build the info files and +# install them in the directory you have defined. + +# INFO_DIRECTORY=/usr/share/info + + +#------------------------------------------------------------------------------ +# Exim log directory and files: Exim creates several log files inside a +# single log directory. You can define the directory and the form of the +# log file name here. If you do not set anything, Exim creates a directory +# called "log" inside its spool directory (see SPOOL_DIRECTORY above) and uses +# the filenames "mainlog", "paniclog", and "rejectlog". If you want to change +# this, you can set LOG_FILE_PATH to a path name containing one occurrence of +# %s. This will be replaced by one of the strings "main", "panic", or "reject" +# to form the final file names. Some installations may want something like this: + +# LOG_FILE_PATH=/var/log/exim_%slog + +# which results in files with names /var/log/exim_mainlog, etc. The directory +# in which the log files are placed must exist; Exim does not try to create +# it for itself. It is also your responsibility to ensure that Exim is capable +# of writing files using this path name. The Exim user (see EXIM_USER above) +# must be able to create and update files in the directory you have specified. + +# You can also configure Exim to use syslog, instead of or as well as log +# files, by settings such as these + +# LOG_FILE_PATH=syslog +# LOG_FILE_PATH=syslog:/var/log/exim_%slog + +# The first of these uses only syslog; the second uses syslog and also writes +# to log files. Do not include white space in such a setting as it messes up +# the building process. + + +#------------------------------------------------------------------------------ +# When logging to syslog, the following option caters for syslog replacements +# that are able to accept log entries longer than the 1024 characters allowed +# by RFC 3164. It is up to you to make sure your syslog daemon can handle this. +# Non-printable characters are usually unacceptable regardless, so log entries +# are still split on newline characters. + +# SYSLOG_LONG_LINES=yes + +# If you are not interested in the process identifier (pid) of the Exim that is +# making the call to syslog, then comment out the following line. + +SYSLOG_LOG_PID=yes + + +#------------------------------------------------------------------------------ +# Cycling log files: this variable specifies the maximum number of old +# log files that are kept by the exicyclog log-cycling script. You don't have +# to use exicyclog. If your operating system has other ways of cycling log +# files, you can use them instead. The exicyclog script isn't run by default; +# you have to set up a cron job for it if you want it. + +EXICYCLOG_MAX=10 + + +#------------------------------------------------------------------------------ +# The compress command is used by the exicyclog script to compress old log +# files. Both the name of the command and the suffix that it adds to files +# need to be defined here. See also the EXICYCLOG_MAX configuration. + +COMPRESS_COMMAND=/usr/bin/gzip +COMPRESS_SUFFIX=gz + + +#------------------------------------------------------------------------------ +# If the exigrep utility is fed compressed log files, it tries to uncompress +# them using this command. + +# Leave it empty to enforce autodetection at runtime: +# ZCAT_COMMAND= +# +# Omit the path if you want to use your system's PATH: +# ZCAT_COMMAND=zcat +# +# Or specify the full pathname: +ZCAT_COMMAND=/usr/bin/zcat + +#------------------------------------------------------------------------------ +# Compiling in support for embedded Perl: If you want to be able to +# use Perl code in Exim's string manipulation language and you have Perl +# (version 5.004 or later) installed, set EXIM_PERL to perl.o. Using embedded +# Perl costs quite a lot of resources. Only do this if you really need it. + +# EXIM_PERL=perl.o + + +#------------------------------------------------------------------------------ +# Support for dynamically-loaded string expansion functions via ${dlfunc. If +# you are using gcc the dynamically-loaded object must be compiled with the +# -shared option, and you will need to add -export-dynamic to EXTRALIBS so +# that the local_scan API is made available by the linker. You may also need +# to add -ldl to EXTRALIBS so that dlopen() is available to Exim. + +# EXPAND_DLFUNC=yes + + +#------------------------------------------------------------------------------ +# Exim has support for PAM (Pluggable Authentication Modules), a facility +# which is available in the latest releases of Solaris and in some GNU/Linux +# distributions (see http://ftp.kernel.org/pub/linux/libs/pam/). The Exim +# support, which is intended for use in conjunction with the SMTP AUTH +# facilities, is included only when requested by the following setting: + +# SUPPORT_PAM=yes + +# You probably need to add -lpam to EXTRALIBS, and in some releases of +# GNU/Linux -ldl is also needed. + + +#------------------------------------------------------------------------------ +# Proxying. +# +# If you may want to use outbound (client-side) proxying, using Socks5, +# uncomment the line below. + +# SUPPORT_SOCKS=yes + +# If you may want to use inbound (server-side) proxying, using Proxy Protocol, +# uncomment the line below. + +# SUPPORT_PROXY=yes + + +#------------------------------------------------------------------------------ +# Internationalisation. +# +# Uncomment the following to include Internationalisation features. This is the +# SMTPUTF8 ESMTP extension, and associated facilities for handling UTF8 domain +# and localparts, per RFC 3490 (IDNA2003). +# You need to have the IDN library installed. +# If you want IDNA2008 mappings per RFCs 5890, 6530 and 6533, you additionally +# need libidn2 and SUPPORT_I18N_2008. + +# SUPPORT_I18N=yes +# LDFLAGS += -lidn +# SUPPORT_I18N_2008=yes +# LDFLAGS += -lidn -lidn2 + + +#------------------------------------------------------------------------------ +# Uncomment the following lines to add SPF support. You need to have libspf2 +# installed on your system (www.libspf2.org). Depending on where it is installed +# you may have to edit the CFLAGS and LDFLAGS lines. + +# SUPPORT_SPF=yes +# CFLAGS += -I/usr/local/include +# LDFLAGS += -lspf2 + + +#------------------------------------------------------------------------------ +# Support for authentication via Radius is also available. The Exim support, +# which is intended for use in conjunction with the SMTP AUTH facilities, +# is included only when requested by setting the following parameter to the +# location of your Radius configuration file: + +# RADIUS_CONFIG_FILE=/etc/radiusclient/radiusclient.conf +# RADIUS_CONFIG_FILE=/etc/radius.conf + +# If you have set RADIUS_CONFIG_FILE, you should also set one of these to +# indicate which RADIUS library is used: + +# RADIUS_LIB_TYPE=RADIUSCLIENT +# RADIUS_LIB_TYPE=RADIUSCLIENTNEW +# RADIUS_LIB_TYPE=RADLIB + +# RADIUSCLIENT is the radiusclient library; you probably need to add +# -lradiusclient to EXTRALIBS. +# +# The API for the radiusclient library was changed at release 0.4.0. +# Unfortunately, the header file does not define a version number that clients +# can use to support both the old and new APIs. If you are using version 0.4.0 +# or later of the radiusclient library, you should use RADIUSCLIENTNEW. +# +# RADLIB is the Radius library that comes with FreeBSD (the header file is +# called radlib.h); you probably need to add -lradius to EXTRALIBS. +# +# If you do not set RADIUS_LIB_TYPE, Exim assumes the radiusclient library, +# using the original API. + + +#------------------------------------------------------------------------------ +# Support for authentication via the Cyrus SASL pwcheck daemon is available. +# Note, however, that pwcheck is now deprecated in favour of saslauthd (see +# next item). The Exim support for pwcheck, which is intented for use in +# conjunction with the SMTP AUTH facilities, is included only when requested by +# setting the following parameter to the location of the pwcheck daemon's +# socket. +# +# There is no need to install all of SASL on your system. You just need to run +# ./configure --with-pwcheck, cd to the pwcheck directory within the sources, +# make and make install. You must create the socket directory (default +# /var/pwcheck) and chown it to Exim's user and group. Once you have installed +# pwcheck, you should arrange for it to be started by root at boot time. + +# CYRUS_PWCHECK_SOCKET=/var/pwcheck/pwcheck + + +#------------------------------------------------------------------------------ +# Support for authentication via the Cyrus SASL saslauthd daemon is available. +# The Exim support, which is intended for use in conjunction with the SMTP AUTH +# facilities, is included only when requested by setting the following +# parameter to the location of the saslauthd daemon's socket. +# +# There is no need to install all of SASL on your system. You just need to run +# ./configure --with-saslauthd (and any other options you need, for example, to +# select or deselect authentication mechanisms), cd to the saslauthd directory +# within the sources, make and make install. You must create the socket +# directory (default /var/state/saslauthd) and chown it to Exim's user and +# group. Once you have installed saslauthd, you should arrange for it to be +# started by root at boot time. + +# CYRUS_SASLAUTHD_SOCKET=/var/state/saslauthd/mux + + +#------------------------------------------------------------------------------ +# TCP wrappers: If you want to use tcpwrappers from within Exim, uncomment +# this setting. See the manual section entitled "Use of tcpwrappers" in the +# chapter on building and installing Exim. +# +# USE_TCP_WRAPPERS=yes +# +# You may well also have to specify a local "include" file and an additional +# library for TCP wrappers, so you probably need something like this: +# +# USE_TCP_WRAPPERS=yes +# CFLAGS=-O -I/usr/local/include +# EXTRALIBS_EXIM=-L/usr/local/lib -lwrap +# +# but of course there may need to be other things in CFLAGS and EXTRALIBS_EXIM +# as well. +# +# To use a name other than exim in the tcpwrappers config file, +# e.g. if you're running multiple daemons with different access lists, +# or multiple MTAs with the same access list, define +# TCP_WRAPPERS_DAEMON_NAME accordingly +# +# TCP_WRAPPERS_DAEMON_NAME="exim" + + +#------------------------------------------------------------------------------ +# The default action of the exim_install script (which is run by "make +# install") is to install the Exim binary with a unique name such as +# exim-4.43-1, and then set up a symbolic link called "exim" to reference it, +# moving the symbolic link from any previous version. If you define NO_SYMLINK +# (the value doesn't matter), the symbolic link is not created or moved. You +# will then have to "turn Exim on" by setting up the link manually. + +# NO_SYMLINK=yes + + +#------------------------------------------------------------------------------ +# Another default action of the install script is to install a default runtime +# configuration file if one does not exist. This configuration has a router for +# expanding system aliases. The default assumes that these aliases are kept +# in the traditional file called /etc/aliases. If such a file does not exist, +# the installation script creates one that contains just comments (no actual +# aliases). The following setting can be changed to specify a different +# location for the system alias file. + +SYSTEM_ALIASES_FILE=/etc/aliases + + +#------------------------------------------------------------------------------ +# There are some testing options (-be, -bt, -bv) that read data from the +# standard input when no arguments are supplied. By default, the input lines +# are read using the standard fgets() function. This does not support line +# editing during interactive input (though the terminal's "erase" character +# works as normal). If your operating system has the readline() function, and +# in addition supports dynamic loading of library functions, you can cause +# Exim to use readline() for the -be testing option (only) by uncommenting the +# following setting. Dynamic loading is used so that the library is loaded only +# when the -be testing option is given; by the time the loading occurs, +# Exim has given up its root privilege and is running as the calling user. This +# is the reason why readline() is NOT supported for -bt and -bv, because Exim +# runs as root or as exim, respectively, for those options. When USE_READLINE +# is "yes", as well as supporting line editing, a history of input lines in the +# current run is maintained. + +# USE_READLINE=yes + +# You may need to add -ldl to EXTRALIBS when you set USE_READLINE=yes. +# Note that this option adds to the size of the Exim binary, because the +# dynamic loading library is not otherwise included. + +# If libreadline is not in the normal library paths, then because Exim is +# setuid you'll need to ensure that the correct directory is stamped into +# the binary so that dlopen will find it. +# Eg, on macOS/Darwin with a third-party install of libreadline, perhaps: + +# EXTRALIBS_EXIM+=-Wl,-rpath,/usr/local/opt/readline/lib + + +#------------------------------------------------------------------------------ +# Uncomment this setting to include IPv6 support. + +# HAVE_IPV6=yes + +############################################################################### +# THINGS YOU ALMOST NEVER NEED TO MENTION # +############################################################################### + +# The settings in this section are available for use in special circumstances. +# In the vast majority of installations you need not change anything below. + + +#------------------------------------------------------------------------------ +# The following commands live in different places in some OS. Either the +# ultimate default settings, or the OS-specific files should already point to +# the right place, but they can be overridden here if necessary. These settings +# are used when building various scripts to ensure that the correct paths are +# used when the scripts are run. They are not used in the Makefile itself. Perl +# is not necessary for running Exim unless you set EXIM_PERL (see above) to get +# it embedded, but there are some utilities that are Perl scripts. If you +# haven't got Perl, Exim will still build and run; you just won't be able to +# use those utilities. + +# CHOWN_COMMAND=/usr/bin/chown +# CHGRP_COMMAND=/usr/bin/chgrp +# CHMOD_COMMAND=/usr/bin/chmod +# MV_COMMAND=/bin/mv +# RM_COMMAND=/bin/rm +# TOUCH_COMMAND=/usr/bin/touch +# PERL_COMMAND=/usr/bin/perl + + +#------------------------------------------------------------------------------ +# The following macro can be used to change the command for building a library +# of functions. By default the "ar" command is used, with options "cq". +# Only in rare circumstances should you need to change this. + +# AR=ar cq + + +#------------------------------------------------------------------------------ +# In some operating systems, the value of the TMPDIR environment variable +# controls where temporary files are created. Exim does not make use of +# temporary files, except when delivering to MBX mailboxes. However, if Exim +# calls any external libraries (e.g. DBM libraries), they may use temporary +# files, and thus be influenced by the value of TMPDIR. For this reason, when +# Exim starts, it checks the environment for TMPDIR, and if it finds it is set, +# it replaces the value with what is defined here. Commenting this setting +# suppresses the check altogether. Older installations call this macro +# just TMPDIR, but this has side effects at build time. At runtime +# TMPDIR is checked as before. + +EXIM_TMPDIR="/tmp" + + +#------------------------------------------------------------------------------ +# The following macros can be used to change the default modes that are used +# by the appendfile transport. In most installations the defaults are just +# fine, and in any case, you can change particular instances of the transport +# at run time if you want. + +# APPENDFILE_MODE=0600 +# APPENDFILE_DIRECTORY_MODE=0700 +# APPENDFILE_LOCKFILE_MODE=0600 + + +#------------------------------------------------------------------------------ +# In some installations there may be multiple machines sharing file systems, +# where a different configuration file is required for Exim on the different +# machines. If CONFIGURE_FILE_USE_NODE is defined, then Exim will first look +# for a configuration file whose name is that defined by CONFIGURE_FILE, +# with the node name obtained by uname() tacked on the end, separated by a +# period (for example, /usr/exim/configure.host.in.some.domain). If this file +# does not exist, then the bare configuration file name is tried. + +# CONFIGURE_FILE_USE_NODE=yes + + +#------------------------------------------------------------------------------ +# In some esoteric configurations two different versions of Exim are run, +# with different setuid values, and different configuration files are required +# to handle the different cases. If CONFIGURE_FILE_USE_EUID is defined, then +# Exim will first look for a configuration file whose name is that defined +# by CONFIGURE_FILE, with the effective uid tacked on the end, separated by +# a period (for example, /usr/exim/configure.0). If this file does not exist, +# then the bare configuration file name is tried. In the case when both +# CONFIGURE_FILE_USE_EUID and CONFIGURE_FILE_USE_NODE are set, four files +# are tried: .., ., ., and . + +# CONFIGURE_FILE_USE_EUID=yes + + +#------------------------------------------------------------------------------ +# The size of the delivery buffers: These specify the sizes (in bytes) of +# the buffers that are used when copying a message from the spool to a +# destination. There is rarely any need to change these values. + +# DELIVER_IN_BUFFER_SIZE=8192 +# DELIVER_OUT_BUFFER_SIZE=8192 + + +#------------------------------------------------------------------------------ +# The mode of the database directory: Exim creates a directory called "db" +# in its spool directory, to hold its databases of hints. This variable +# determines the mode of the created directory. The default value in the +# source is 0750. + +# EXIMDB_DIRECTORY_MODE=0750 + + +#------------------------------------------------------------------------------ +# Database file mode: The mode of files created in the "db" directory defaults +# to 0640 in the source, and can be changed here. + +# EXIMDB_MODE=0640 + + +#------------------------------------------------------------------------------ +# Database lock file mode: The mode of zero-length files created in the "db" +# directory to use for locking purposes defaults to 0640 in the source, and +# can be changed here. + +# EXIMDB_LOCKFILE_MODE=0640 + + +#------------------------------------------------------------------------------ +# This parameter sets the maximum length of the header portion of a message +# that Exim is prepared to process. The default setting is one megabyte. The +# limit exists in order to catch rogue mailers that might connect to your SMTP +# port, start off a header line, and then just pump junk at it for ever. The +# message_size_limit option would also catch this, but it may not be set. +# The value set here is the default; it can be changed at runtime. + +# HEADER_MAXSIZE="(1024*1024)" + + +#------------------------------------------------------------------------------ +# The mode of the input directory: The input directory is where messages are +# kept while awaiting delivery. Exim creates it if necessary, using a mode +# which can be defined here (default 0750). + +# INPUT_DIRECTORY_MODE=0750 + + +#------------------------------------------------------------------------------ +# The mode of Exim's log directory, when it is created by Exim inside the spool +# directory, defaults to 0750 but can be changed here. + +# LOG_DIRECTORY_MODE=0750 + + +#------------------------------------------------------------------------------ +# The log files themselves are created as required, with a mode that defaults +# to 0640, but which can be changed here. + +# LOG_MODE=0640 + + +#------------------------------------------------------------------------------ +# The TESTDB lookup is for performing tests on the handling of lookup results, +# and is not useful for general running. It should be included only when +# debugging the code of Exim. + +# LOOKUP_TESTDB=yes + + +#------------------------------------------------------------------------------ +# /bin/sh is used by default as the shell in which to run commands that are +# defined in the makefiles. This can be changed if necessary, by uncommenting +# this line and specifying another shell, but note that a Bourne-compatible +# shell is expected. + +# MAKE_SHELL=/bin/sh + + +#------------------------------------------------------------------------------ +# The maximum number of named lists of each type (address, domain, host, and +# local part) can be increased by changing this value. It should be set to +# a multiple of 16. + +# MAX_NAMED_LIST=16 + + +#------------------------------------------------------------------------------ +# Network interfaces: Unless you set the local_interfaces option in the runtime +# configuration file to restrict Exim to certain interfaces only, it will run +# code to find all the interfaces there are on your host. Unfortunately, +# the call to the OS that does this requires a buffer large enough to hold +# data for all the interfaces - it was designed in the days when a host rarely +# had more than three or four interfaces. Nowadays hosts can have very many +# virtual interfaces running on the same hardware. If you have more than 250 +# virtual interfaces, you will need to uncomment this setting and increase the +# value. + +# MAXINTERFACES=250 + + +#------------------------------------------------------------------------------ +# Per-message logs: While a message is in the process of being delivered, +# comments on its progress are written to a message log, for the benefit of +# human administrators. These logs are held in a directory called "msglog" +# in the spool directory. Its mode defaults to 0750, but can be changed here. +# The message log directory is also used for storing files that are used by +# transports for returning data to a message's sender (see the "return_output" +# option for transports). + +# MSGLOG_DIRECTORY_MODE=0750 + + +#------------------------------------------------------------------------------ +# There are three options which are used when compiling the Perl interface and +# when linking with Perl. The default values for these are placed automatically +# at the head of the Makefile by the script which builds it. However, if you +# want to override them, you can do so here. + +# PERL_CC= +# PERL_CCOPTS= +# PERL_LIBS= + + +#------------------------------------------------------------------------------ +# If you wish to disable valgrind in the binary, define NVALGRIND=1. +# This should not be needed. + +# NVALGRIND=1 + +#------------------------------------------------------------------------------ +# Identifying the daemon: When an Exim daemon starts up, it writes its pid +# (process id) to a file so that it can easily be identified. The path of the +# file can be specified here. Some installations may want something like this: + +# PID_FILE_PATH=/var/lock/exim.pid + +# If PID_FILE_PATH is not defined, Exim writes a file in its spool directory +# using the name "exim-daemon.pid". + +# If you start up a daemon without the -bd option (for example, with just +# the -q15m option), a pid file is not written. Also, if you override the +# configuration file with the -oX option, no pid file is written. In other +# words, the pid file is written only for a "standard" daemon. + + +#------------------------------------------------------------------------------ +# If Exim creates the spool directory, it is given this mode, defaulting in the +# source to 0750. + +# SPOOL_DIRECTORY_MODE=0750 + + +#------------------------------------------------------------------------------ +# The mode of files on the input spool which hold the contents of messages can +# be changed here. The default is 0640 so that information from the spool is +# available to anyone who is a member of the Exim group. + +# SPOOL_MODE=0640 + + +#------------------------------------------------------------------------------ +# Moving frozen messages: If the following is uncommented, Exim is compiled +# with support for automatically moving frozen messages out of the main spool +# directory, a facility that is found useful by some large installations. A +# run time option is required to cause the moving actually to occur. Such +# messages become "invisible" to the normal management tools. + +# SUPPORT_MOVE_FROZEN_MESSAGES=yes + + +#------------------------------------------------------------------------------ +# Expanding match_* second parameters: BE CAREFUL IF ENABLING THIS! +# It has proven too easy in practice for administrators to configure security +# problems into their Exim install, by treating match_domain{}{} and friends +# as a form of string comparison, where the second string comes from untrusted +# data. Because these options take lists, which can include lookup;LOOKUPDATA +# style elements, a foe can then cause Exim to, eg, execute an arbitrary MySQL +# query, dropping tables. +# From Exim 4.77 onwards, the second parameter is not expanded; it can still +# be a list literal, or a macro, or a named list reference. There is also +# the new expansion condition "inlisti" which does expand the second parameter, +# but treats it as a list of strings; also, there's "eqi" which is probably +# what is normally wanted. +# +# If you really need to have the old behaviour, know what you are doing and +# will not complain if your system is compromised as a result of doing so, then +# uncomment this option to get the old behaviour back. + +# EXPAND_LISTMATCH_RHS=yes + +#------------------------------------------------------------------------------ +# Disabling the use of fsync(): DO NOT UNCOMMENT THE FOLLOWING LINE unless you +# really, really, really know what you are doing. And even then, think again. +# You should never uncomment this when compiling a binary for distribution. +# Use it only when compiling Exim for your own use. +# +# Uncommenting this line enables the use of a runtime option called +# disable_fsync, which can be used to stop Exim using fsync() to ensure that +# files are written to disc before proceeding. When this is disabled, crashes +# and hardware problems such as power outages can cause data to be lost. This +# feature should only be used in very exceptional circumstances. YOU HAVE BEEN +# WARNED. + +# ENABLE_DISABLE_FSYNC=yes + +#------------------------------------------------------------------------------ +# For development, add this to include code to time various stages and report. +# CFLAGS += -DMEASURE_TIMING + +# For a very slightly smaller build, for constrained systems, uncomment this. +# The feature involved is purely for debugging. + +# DISABLE_CLIENT_CMD_LOG=yes + +# End of EDITME for Exim 4. diff --git a/src/acl.c b/src/acl.c new file mode 100644 index 0000000..fb78a7b --- /dev/null +++ b/src/acl.c @@ -0,0 +1,4896 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Code for handling Access Control Lists (ACLs) */ + +#include "exim.h" + +#ifndef MACRO_PREDEF + +/* Default callout timeout */ + +#define CALLOUT_TIMEOUT_DEFAULT 30 + +/* Default quota cache TTLs */ + +#define QUOTA_POS_DEFAULT (5*60) +#define QUOTA_NEG_DEFAULT (60*60) + + +/* ACL verb codes - keep in step with the table of verbs that follows */ + +enum { ACL_ACCEPT, ACL_DEFER, ACL_DENY, ACL_DISCARD, ACL_DROP, ACL_REQUIRE, + ACL_WARN }; + +/* ACL verbs */ + +static uschar *verbs[] = { + [ACL_ACCEPT] = US"accept", + [ACL_DEFER] = US"defer", + [ACL_DENY] = US"deny", + [ACL_DISCARD] = US"discard", + [ACL_DROP] = US"drop", + [ACL_REQUIRE] = US"require", + [ACL_WARN] = US"warn" +}; + +/* For each verb, the conditions for which "message" or "log_message" are used +are held as a bitmap. This is to avoid expanding the strings unnecessarily. For +"accept", the FAIL case is used only after "endpass", but that is selected in +the code. */ + +static int msgcond[] = { + [ACL_ACCEPT] = BIT(OK) | BIT(FAIL) | BIT(FAIL_DROP), + [ACL_DEFER] = BIT(OK), + [ACL_DENY] = BIT(OK), + [ACL_DISCARD] = BIT(OK) | BIT(FAIL) | BIT(FAIL_DROP), + [ACL_DROP] = BIT(OK), + [ACL_REQUIRE] = BIT(FAIL) | BIT(FAIL_DROP), + [ACL_WARN] = BIT(OK) + }; + +#endif + +/* ACL condition and modifier codes - keep in step with the table that +follows. +down. */ + +enum { ACLC_ACL, + ACLC_ADD_HEADER, + ACLC_AUTHENTICATED, +#ifdef EXPERIMENTAL_BRIGHTMAIL + ACLC_BMI_OPTIN, +#endif + ACLC_CONDITION, + ACLC_CONTINUE, + ACLC_CONTROL, +#ifdef EXPERIMENTAL_DCC + ACLC_DCC, +#endif +#ifdef WITH_CONTENT_SCAN + ACLC_DECODE, +#endif + ACLC_DELAY, +#ifndef DISABLE_DKIM + ACLC_DKIM_SIGNER, + ACLC_DKIM_STATUS, +#endif +#ifdef SUPPORT_DMARC + ACLC_DMARC_STATUS, +#endif + ACLC_DNSLISTS, + ACLC_DOMAINS, + ACLC_ENCRYPTED, + ACLC_ENDPASS, + ACLC_HOSTS, + ACLC_LOCAL_PARTS, + ACLC_LOG_MESSAGE, + ACLC_LOG_REJECT_TARGET, + ACLC_LOGWRITE, +#ifdef WITH_CONTENT_SCAN + ACLC_MALWARE, +#endif + ACLC_MESSAGE, +#ifdef WITH_CONTENT_SCAN + ACLC_MIME_REGEX, +#endif + ACLC_QUEUE, + ACLC_RATELIMIT, + ACLC_RECIPIENTS, +#ifdef WITH_CONTENT_SCAN + ACLC_REGEX, +#endif + ACLC_REMOVE_HEADER, + ACLC_SEEN, + ACLC_SENDER_DOMAINS, + ACLC_SENDERS, + ACLC_SET, +#ifdef WITH_CONTENT_SCAN + ACLC_SPAM, +#endif +#ifdef SUPPORT_SPF + ACLC_SPF, + ACLC_SPF_GUESS, +#endif + ACLC_UDPSEND, + ACLC_VERIFY }; + +/* ACL conditions/modifiers: "delay", "control", "continue", "endpass", +"message", "log_message", "log_reject_target", "logwrite", "queue" and "set" are +modifiers that look like conditions but always return TRUE. They are used for +their side effects. Do not invent new modifier names that result in one name +being the prefix of another; the binary-search in the list will go wrong. */ + +typedef struct condition_def { + uschar *name; + +/* Flag to indicate the condition/modifier has a string expansion done +at the outer level. In the other cases, expansion already occurs in the +checking functions. */ + BOOL expand_at_top:1; + + BOOL is_modifier:1; + +/* Bit map vector of which conditions and modifiers are not allowed at certain +times. For each condition and modifier, there's a bitmap of dis-allowed times. +For some, it is easier to specify the negation of a small number of allowed +times. */ + unsigned forbids; + +} condition_def; + +static condition_def conditions[] = { + [ACLC_ACL] = { US"acl", FALSE, FALSE, 0 }, + + [ACLC_ADD_HEADER] = { US"add_header", TRUE, TRUE, + (unsigned int) + ~(ACL_BIT_MAIL | ACL_BIT_RCPT | + ACL_BIT_PREDATA | ACL_BIT_DATA | +#ifndef DISABLE_PRDR + ACL_BIT_PRDR | +#endif + ACL_BIT_MIME | ACL_BIT_NOTSMTP | + ACL_BIT_DKIM | + ACL_BIT_NOTSMTP_START), + }, + + [ACLC_AUTHENTICATED] = { US"authenticated", FALSE, FALSE, + ACL_BIT_NOTSMTP | ACL_BIT_NOTSMTP_START | + ACL_BIT_CONNECT | ACL_BIT_HELO, + }, +#ifdef EXPERIMENTAL_BRIGHTMAIL + [ACLC_BMI_OPTIN] = { US"bmi_optin", TRUE, TRUE, + ACL_BIT_AUTH | + ACL_BIT_CONNECT | ACL_BIT_HELO | + ACL_BIT_DATA | ACL_BIT_MIME | +# ifndef DISABLE_PRDR + ACL_BIT_PRDR | +# endif + ACL_BIT_ETRN | ACL_BIT_EXPN | + ACL_BIT_MAILAUTH | + ACL_BIT_MAIL | ACL_BIT_STARTTLS | + ACL_BIT_VRFY | ACL_BIT_PREDATA | + ACL_BIT_NOTSMTP_START, + }, +#endif + [ACLC_CONDITION] = { US"condition", TRUE, FALSE, 0 }, + [ACLC_CONTINUE] = { US"continue", TRUE, TRUE, 0 }, + + /* Certain types of control are always allowed, so we let it through + always and check in the control processing itself. */ + [ACLC_CONTROL] = { US"control", TRUE, TRUE, 0 }, + +#ifdef EXPERIMENTAL_DCC + [ACLC_DCC] = { US"dcc", TRUE, FALSE, + (unsigned int) + ~(ACL_BIT_DATA | +# ifndef DISABLE_PRDR + ACL_BIT_PRDR | +# endif + ACL_BIT_NOTSMTP), + }, +#endif +#ifdef WITH_CONTENT_SCAN + [ACLC_DECODE] = { US"decode", TRUE, FALSE, (unsigned int) ~ACL_BIT_MIME }, + +#endif + [ACLC_DELAY] = { US"delay", TRUE, TRUE, ACL_BIT_NOTQUIT }, +#ifndef DISABLE_DKIM + [ACLC_DKIM_SIGNER] = { US"dkim_signers", TRUE, FALSE, (unsigned int) ~ACL_BIT_DKIM }, + [ACLC_DKIM_STATUS] = { US"dkim_status", TRUE, FALSE, (unsigned int) ~ACL_BIT_DKIM }, +#endif +#ifdef SUPPORT_DMARC + [ACLC_DMARC_STATUS] = { US"dmarc_status", TRUE, FALSE, (unsigned int) ~ACL_BIT_DATA }, +#endif + + /* Explicit key lookups can be made in non-smtp ACLs so pass + always and check in the verify processing itself. */ + [ACLC_DNSLISTS] = { US"dnslists", TRUE, FALSE, 0 }, + + [ACLC_DOMAINS] = { US"domains", FALSE, FALSE, + (unsigned int) + ~(ACL_BIT_RCPT | ACL_BIT_VRFY +#ifndef DISABLE_PRDR + |ACL_BIT_PRDR +#endif + ), + }, + [ACLC_ENCRYPTED] = { US"encrypted", FALSE, FALSE, + ACL_BIT_NOTSMTP | ACL_BIT_NOTSMTP_START | + ACL_BIT_HELO, + }, + + [ACLC_ENDPASS] = { US"endpass", TRUE, TRUE, 0 }, + + [ACLC_HOSTS] = { US"hosts", FALSE, FALSE, + ACL_BIT_NOTSMTP | ACL_BIT_NOTSMTP_START, + }, + [ACLC_LOCAL_PARTS] = { US"local_parts", FALSE, FALSE, + (unsigned int) + ~(ACL_BIT_RCPT | ACL_BIT_VRFY +#ifndef DISABLE_PRDR + | ACL_BIT_PRDR +#endif + ), + }, + + [ACLC_LOG_MESSAGE] = { US"log_message", TRUE, TRUE, 0 }, + [ACLC_LOG_REJECT_TARGET] = { US"log_reject_target", TRUE, TRUE, 0 }, + [ACLC_LOGWRITE] = { US"logwrite", TRUE, TRUE, 0 }, + +#ifdef WITH_CONTENT_SCAN + [ACLC_MALWARE] = { US"malware", TRUE, FALSE, + (unsigned int) + ~(ACL_BIT_DATA | +# ifndef DISABLE_PRDR + ACL_BIT_PRDR | +# endif + ACL_BIT_NOTSMTP), + }, +#endif + + [ACLC_MESSAGE] = { US"message", TRUE, TRUE, 0 }, +#ifdef WITH_CONTENT_SCAN + [ACLC_MIME_REGEX] = { US"mime_regex", TRUE, FALSE, (unsigned int) ~ACL_BIT_MIME }, +#endif + + [ACLC_QUEUE] = { US"queue", TRUE, TRUE, + ACL_BIT_NOTSMTP | +#ifndef DISABLE_PRDR + ACL_BIT_PRDR | +#endif + ACL_BIT_DATA, + }, + + [ACLC_RATELIMIT] = { US"ratelimit", TRUE, FALSE, 0 }, + [ACLC_RECIPIENTS] = { US"recipients", FALSE, FALSE, (unsigned int) ~ACL_BIT_RCPT }, + +#ifdef WITH_CONTENT_SCAN + [ACLC_REGEX] = { US"regex", TRUE, FALSE, + (unsigned int) + ~(ACL_BIT_DATA | +# ifndef DISABLE_PRDR + ACL_BIT_PRDR | +# endif + ACL_BIT_NOTSMTP | + ACL_BIT_MIME), + }, + +#endif + [ACLC_REMOVE_HEADER] = { US"remove_header", TRUE, TRUE, + (unsigned int) + ~(ACL_BIT_MAIL|ACL_BIT_RCPT | + ACL_BIT_PREDATA | ACL_BIT_DATA | +#ifndef DISABLE_PRDR + ACL_BIT_PRDR | +#endif + ACL_BIT_MIME | ACL_BIT_NOTSMTP | + ACL_BIT_NOTSMTP_START), + }, + [ACLC_SEEN] = { US"seen", TRUE, FALSE, 0 }, + [ACLC_SENDER_DOMAINS] = { US"sender_domains", FALSE, FALSE, + ACL_BIT_AUTH | ACL_BIT_CONNECT | + ACL_BIT_HELO | + ACL_BIT_MAILAUTH | ACL_BIT_QUIT | + ACL_BIT_ETRN | ACL_BIT_EXPN | + ACL_BIT_STARTTLS | ACL_BIT_VRFY, + }, + [ACLC_SENDERS] = { US"senders", FALSE, FALSE, + ACL_BIT_AUTH | ACL_BIT_CONNECT | + ACL_BIT_HELO | + ACL_BIT_MAILAUTH | ACL_BIT_QUIT | + ACL_BIT_ETRN | ACL_BIT_EXPN | + ACL_BIT_STARTTLS | ACL_BIT_VRFY, + }, + + [ACLC_SET] = { US"set", TRUE, TRUE, 0 }, + +#ifdef WITH_CONTENT_SCAN + [ACLC_SPAM] = { US"spam", TRUE, FALSE, + (unsigned int) ~(ACL_BIT_DATA | +# ifndef DISABLE_PRDR + ACL_BIT_PRDR | +# endif + ACL_BIT_NOTSMTP), + }, +#endif +#ifdef SUPPORT_SPF + [ACLC_SPF] = { US"spf", TRUE, FALSE, + ACL_BIT_AUTH | ACL_BIT_CONNECT | + ACL_BIT_HELO | ACL_BIT_MAILAUTH | + ACL_BIT_ETRN | ACL_BIT_EXPN | + ACL_BIT_STARTTLS | ACL_BIT_VRFY | + ACL_BIT_NOTSMTP | ACL_BIT_NOTSMTP_START, + }, + [ACLC_SPF_GUESS] = { US"spf_guess", TRUE, FALSE, + ACL_BIT_AUTH | ACL_BIT_CONNECT | + ACL_BIT_HELO | ACL_BIT_MAILAUTH | + ACL_BIT_ETRN | ACL_BIT_EXPN | + ACL_BIT_STARTTLS | ACL_BIT_VRFY | + ACL_BIT_NOTSMTP | ACL_BIT_NOTSMTP_START, + }, +#endif + [ACLC_UDPSEND] = { US"udpsend", TRUE, TRUE, 0 }, + + /* Certain types of verify are always allowed, so we let it through + always and check in the verify function itself */ + [ACLC_VERIFY] = { US"verify", TRUE, FALSE, 0 }, +}; + + +#ifdef MACRO_PREDEF +# include "macro_predef.h" +void +features_acl(void) +{ +for (condition_def * c = conditions; c < conditions + nelem(conditions); c++) + { + uschar buf[64], * p, * s; + int n = sprintf(CS buf, "_ACL_%s_", c->is_modifier ? "MOD" : "COND"); + for (p = buf + n, s = c->name; *s; s++) *p++ = toupper(*s); + *p = '\0'; + builtin_macro_create(buf); + } +} +#endif + + +#ifndef MACRO_PREDEF + +/* Return values from decode_control(); used as index so keep in step +with the controls_list table that follows! */ + +enum { + CONTROL_AUTH_UNADVERTISED, +#ifdef EXPERIMENTAL_BRIGHTMAIL + CONTROL_BMI_RUN, +#endif + CONTROL_CASEFUL_LOCAL_PART, + CONTROL_CASELOWER_LOCAL_PART, + CONTROL_CUTTHROUGH_DELIVERY, + CONTROL_DEBUG, +#ifndef DISABLE_DKIM + CONTROL_DKIM_VERIFY, +#endif +#ifdef SUPPORT_DMARC + CONTROL_DMARC_VERIFY, + CONTROL_DMARC_FORENSIC, +#endif + CONTROL_DSCP, + CONTROL_ENFORCE_SYNC, + CONTROL_ERROR, /* pseudo-value for decode errors */ + CONTROL_FAKEDEFER, + CONTROL_FAKEREJECT, + CONTROL_FREEZE, + + CONTROL_NO_CALLOUT_FLUSH, + CONTROL_NO_DELAY_FLUSH, + CONTROL_NO_ENFORCE_SYNC, +#ifdef WITH_CONTENT_SCAN + CONTROL_NO_MBOX_UNSPOOL, +#endif + CONTROL_NO_MULTILINE, + CONTROL_NO_PIPELINING, + + CONTROL_QUEUE, + CONTROL_SUBMISSION, + CONTROL_SUPPRESS_LOCAL_FIXUPS, +#ifdef SUPPORT_I18N + CONTROL_UTF8_DOWNCONVERT, +#endif +}; + + + +/* Structure listing various control arguments, with their characteristics. +For each control, there's a bitmap of dis-allowed times. For some, it is easier +to specify the negation of a small number of allowed times. */ + +typedef struct control_def { + uschar *name; + BOOL has_option; /* Has /option(s) following */ + unsigned forbids; /* bitmap of dis-allowed times */ +} control_def; + +static control_def controls_list[] = { + /* name has_option forbids */ +[CONTROL_AUTH_UNADVERTISED] = + { US"allow_auth_unadvertised", FALSE, + (unsigned) + ~(ACL_BIT_CONNECT | ACL_BIT_HELO) + }, +#ifdef EXPERIMENTAL_BRIGHTMAIL +[CONTROL_BMI_RUN] = + { US"bmi_run", FALSE, 0 }, +#endif +[CONTROL_CASEFUL_LOCAL_PART] = + { US"caseful_local_part", FALSE, (unsigned) ~ACL_BIT_RCPT }, +[CONTROL_CASELOWER_LOCAL_PART] = + { US"caselower_local_part", FALSE, (unsigned) ~ACL_BIT_RCPT }, +[CONTROL_CUTTHROUGH_DELIVERY] = + { US"cutthrough_delivery", TRUE, 0 }, +[CONTROL_DEBUG] = + { US"debug", TRUE, 0 }, + +#ifndef DISABLE_DKIM +[CONTROL_DKIM_VERIFY] = + { US"dkim_disable_verify", FALSE, + ACL_BIT_DATA | ACL_BIT_NOTSMTP | +# ifndef DISABLE_PRDR + ACL_BIT_PRDR | +# endif + ACL_BIT_NOTSMTP_START + }, +#endif + +#ifdef SUPPORT_DMARC +[CONTROL_DMARC_VERIFY] = + { US"dmarc_disable_verify", FALSE, + ACL_BIT_DATA | ACL_BIT_NOTSMTP | ACL_BIT_NOTSMTP_START + }, +[CONTROL_DMARC_FORENSIC] = + { US"dmarc_enable_forensic", FALSE, + ACL_BIT_DATA | ACL_BIT_NOTSMTP | ACL_BIT_NOTSMTP_START + }, +#endif + +[CONTROL_DSCP] = + { US"dscp", TRUE, + ACL_BIT_NOTSMTP | ACL_BIT_NOTSMTP_START | ACL_BIT_NOTQUIT + }, +[CONTROL_ENFORCE_SYNC] = + { US"enforce_sync", FALSE, + ACL_BIT_NOTSMTP | ACL_BIT_NOTSMTP_START + }, + + /* Pseudo-value for decode errors */ +[CONTROL_ERROR] = + { US"error", FALSE, 0 }, + +[CONTROL_FAKEDEFER] = + { US"fakedefer", TRUE, + (unsigned) + ~(ACL_BIT_MAIL | ACL_BIT_RCPT | + ACL_BIT_PREDATA | ACL_BIT_DATA | +#ifndef DISABLE_PRDR + ACL_BIT_PRDR | +#endif + ACL_BIT_MIME) + }, +[CONTROL_FAKEREJECT] = + { US"fakereject", TRUE, + (unsigned) + ~(ACL_BIT_MAIL | ACL_BIT_RCPT | + ACL_BIT_PREDATA | ACL_BIT_DATA | +#ifndef DISABLE_PRDR + ACL_BIT_PRDR | +#endif + ACL_BIT_MIME) + }, +[CONTROL_FREEZE] = + { US"freeze", TRUE, + (unsigned) + ~(ACL_BIT_MAIL | ACL_BIT_RCPT | + ACL_BIT_PREDATA | ACL_BIT_DATA | + // ACL_BIT_PRDR| /* Not allow one user to freeze for all */ + ACL_BIT_NOTSMTP | ACL_BIT_MIME) + }, + +[CONTROL_NO_CALLOUT_FLUSH] = + { US"no_callout_flush", FALSE, + ACL_BIT_NOTSMTP | ACL_BIT_NOTSMTP_START + }, +[CONTROL_NO_DELAY_FLUSH] = + { US"no_delay_flush", FALSE, + ACL_BIT_NOTSMTP | ACL_BIT_NOTSMTP_START + }, + +[CONTROL_NO_ENFORCE_SYNC] = + { US"no_enforce_sync", FALSE, + ACL_BIT_NOTSMTP | ACL_BIT_NOTSMTP_START + }, +#ifdef WITH_CONTENT_SCAN +[CONTROL_NO_MBOX_UNSPOOL] = + { US"no_mbox_unspool", FALSE, + (unsigned) + ~(ACL_BIT_MAIL | ACL_BIT_RCPT | + ACL_BIT_PREDATA | ACL_BIT_DATA | + // ACL_BIT_PRDR| /* Not allow one user to freeze for all */ + ACL_BIT_MIME) + }, +#endif +[CONTROL_NO_MULTILINE] = + { US"no_multiline_responses", FALSE, + ACL_BIT_NOTSMTP | ACL_BIT_NOTSMTP_START + }, +[CONTROL_NO_PIPELINING] = + { US"no_pipelining", FALSE, + ACL_BIT_NOTSMTP | ACL_BIT_NOTSMTP_START + }, + +[CONTROL_QUEUE] = + { US"queue", TRUE, + (unsigned) + ~(ACL_BIT_MAIL | ACL_BIT_RCPT | + ACL_BIT_PREDATA | ACL_BIT_DATA | + // ACL_BIT_PRDR| /* Not allow one user to freeze for all */ + ACL_BIT_NOTSMTP | ACL_BIT_MIME) + }, + +[CONTROL_SUBMISSION] = + { US"submission", TRUE, + (unsigned) + ~(ACL_BIT_MAIL | ACL_BIT_RCPT | ACL_BIT_PREDATA) + }, +[CONTROL_SUPPRESS_LOCAL_FIXUPS] = + { US"suppress_local_fixups", FALSE, + (unsigned) + ~(ACL_BIT_MAIL | ACL_BIT_RCPT | ACL_BIT_PREDATA | + ACL_BIT_NOTSMTP_START) + }, +#ifdef SUPPORT_I18N +[CONTROL_UTF8_DOWNCONVERT] = + { US"utf8_downconvert", TRUE, (unsigned) ~(ACL_BIT_RCPT | ACL_BIT_VRFY) + } +#endif +}; + +/* Support data structures for Client SMTP Authorization. acl_verify_csa() +caches its result in a tree to avoid repeated DNS queries. The result is an +integer code which is used as an index into the following tables of +explanatory strings and verification return codes. */ + +static tree_node *csa_cache = NULL; + +enum { CSA_UNKNOWN, CSA_OK, CSA_DEFER_SRV, CSA_DEFER_ADDR, + CSA_FAIL_EXPLICIT, CSA_FAIL_DOMAIN, CSA_FAIL_NOADDR, CSA_FAIL_MISMATCH }; + +/* The acl_verify_csa() return code is translated into an acl_verify() return +code using the following table. It is OK unless the client is definitely not +authorized. This is because CSA is supposed to be optional for sending sites, +so recipients should not be too strict about checking it - especially because +DNS problems are quite likely to occur. It's possible to use $csa_status in +further ACL conditions to distinguish ok, unknown, and defer if required, but +the aim is to make the usual configuration simple. */ + +static int csa_return_code[] = { + [CSA_UNKNOWN] = OK, + [CSA_OK] = OK, + [CSA_DEFER_SRV] = OK, + [CSA_DEFER_ADDR] = OK, + [CSA_FAIL_EXPLICIT] = FAIL, + [CSA_FAIL_DOMAIN] = FAIL, + [CSA_FAIL_NOADDR] = FAIL, + [CSA_FAIL_MISMATCH] = FAIL +}; + +static uschar *csa_status_string[] = { + [CSA_UNKNOWN] = US"unknown", + [CSA_OK] = US"ok", + [CSA_DEFER_SRV] = US"defer", + [CSA_DEFER_ADDR] = US"defer", + [CSA_FAIL_EXPLICIT] = US"fail", + [CSA_FAIL_DOMAIN] = US"fail", + [CSA_FAIL_NOADDR] = US"fail", + [CSA_FAIL_MISMATCH] = US"fail" +}; + +static uschar *csa_reason_string[] = { + [CSA_UNKNOWN] = US"unknown", + [CSA_OK] = US"ok", + [CSA_DEFER_SRV] = US"deferred (SRV lookup failed)", + [CSA_DEFER_ADDR] = US"deferred (target address lookup failed)", + [CSA_FAIL_EXPLICIT] = US"failed (explicit authorization required)", + [CSA_FAIL_DOMAIN] = US"failed (host name not authorized)", + [CSA_FAIL_NOADDR] = US"failed (no authorized addresses)", + [CSA_FAIL_MISMATCH] = US"failed (client address mismatch)" +}; + +/* Options for the ratelimit condition. Note that there are two variants of +the per_rcpt option, depending on the ACL that is used to measure the rate. +However any ACL must be able to look up per_rcpt rates in /noupdate mode, +so the two variants must have the same internal representation as well as +the same configuration string. */ + +enum { + RATE_PER_WHAT, RATE_PER_CLASH, RATE_PER_ADDR, RATE_PER_BYTE, RATE_PER_CMD, + RATE_PER_CONN, RATE_PER_MAIL, RATE_PER_RCPT, RATE_PER_ALLRCPTS +}; + +#define RATE_SET(var,new) \ + (((var) == RATE_PER_WHAT) ? ((var) = RATE_##new) : ((var) = RATE_PER_CLASH)) + +static uschar *ratelimit_option_string[] = { + [RATE_PER_WHAT] = US"?", + [RATE_PER_CLASH] = US"!", + [RATE_PER_ADDR] = US"per_addr", + [RATE_PER_BYTE] = US"per_byte", + [RATE_PER_CMD] = US"per_cmd", + [RATE_PER_CONN] = US"per_conn", + [RATE_PER_MAIL] = US"per_mail", + [RATE_PER_RCPT] = US"per_rcpt", + [RATE_PER_ALLRCPTS] = US"per_rcpt" +}; + +/* Enable recursion between acl_check_internal() and acl_check_condition() */ + +static int acl_check_wargs(int, address_item *, const uschar *, uschar **, + uschar **); + +static acl_block * acl_current = NULL; + + +/************************************************* +* Find control in list * +*************************************************/ + +/* The lists are always in order, so binary chop can be used. + +Arguments: + name the control name to search for + ol the first entry in the control list + last one more than the offset of the last entry in the control list + +Returns: index of a control entry, or -1 if not found +*/ + +static int +find_control(const uschar * name, control_def * ol, int last) +{ +for (int first = 0; last > first; ) + { + int middle = (first + last)/2; + uschar * s = ol[middle].name; + int c = Ustrncmp(name, s, Ustrlen(s)); + if (c == 0) return middle; + else if (c > 0) first = middle + 1; + else last = middle; + } +return -1; +} + + + +/************************************************* +* Pick out condition from list * +*************************************************/ + +/* Use a binary chop method + +Arguments: + name name to find + list list of conditions + end size of list + +Returns: offset in list, or -1 if not found +*/ + +static int +acl_checkcondition(uschar * name, condition_def * list, int end) +{ +for (int start = 0; start < end; ) + { + int mid = (start + end)/2; + int c = Ustrcmp(name, list[mid].name); + if (c == 0) return mid; + if (c < 0) end = mid; + else start = mid + 1; + } +return -1; +} + + +/************************************************* +* Pick out name from list * +*************************************************/ + +/* Use a binary chop method + +Arguments: + name name to find + list list of names + end size of list + +Returns: offset in list, or -1 if not found +*/ + +static int +acl_checkname(uschar *name, uschar **list, int end) +{ +for (int start = 0; start < end; ) + { + int mid = (start + end)/2; + int c = Ustrcmp(name, list[mid]); + if (c == 0) return mid; + if (c < 0) end = mid; else start = mid + 1; + } + +return -1; +} + + +/************************************************* +* Read and parse one ACL * +*************************************************/ + +/* This function is called both from readconf in order to parse the ACLs in the +configuration file, and also when an ACL is encountered dynamically (e.g. as +the result of an expansion). It is given a function to call in order to +retrieve the lines of the ACL. This function handles skipping comments and +blank lines (where relevant). + +Arguments: + func function to get next line of ACL + error where to put an error message + +Returns: pointer to ACL, or NULL + NULL can be legal (empty ACL); in this case error will be NULL +*/ + +acl_block * +acl_read(uschar *(*func)(void), uschar **error) +{ +acl_block *yield = NULL; +acl_block **lastp = &yield; +acl_block *this = NULL; +acl_condition_block *cond; +acl_condition_block **condp = NULL; +uschar * s; + +*error = NULL; + +while ((s = (*func)())) + { + int v, c; + BOOL negated = FALSE; + uschar *saveline = s; + uschar name[EXIM_DRIVERNAME_MAX]; + + /* Conditions (but not verbs) are allowed to be negated by an initial + exclamation mark. */ + + if (Uskip_whitespace(&s) == '!') + { + negated = TRUE; + s++; + } + + /* Read the name of a verb or a condition, or the start of a new ACL, which + can be started by a name, or by a macro definition. */ + + s = readconf_readname(name, sizeof(name), s); + if (*s == ':' || (isupper(name[0]) && *s == '=')) return yield; + + /* If a verb is unrecognized, it may be another condition or modifier that + continues the previous verb. */ + + if ((v = acl_checkname(name, verbs, nelem(verbs))) < 0) + { + if (!this) + { + *error = string_sprintf("unknown ACL verb \"%s\" in \"%s\"", name, + saveline); + return NULL; + } + } + + /* New verb */ + + else + { + if (negated) + { + *error = string_sprintf("malformed ACL line \"%s\"", saveline); + return NULL; + } + this = store_get(sizeof(acl_block), GET_UNTAINTED); + *lastp = this; + lastp = &(this->next); + this->next = NULL; + this->condition = NULL; + this->verb = v; + this->srcline = config_lineno; /* for debug output */ + this->srcfile = config_filename; /**/ + condp = &(this->condition); + if (*s == 0) continue; /* No condition on this line */ + if (*s == '!') + { + negated = TRUE; + s++; + } + s = readconf_readname(name, sizeof(name), s); /* Condition name */ + } + + /* Handle a condition or modifier. */ + + if ((c = acl_checkcondition(name, conditions, nelem(conditions))) < 0) + { + *error = string_sprintf("unknown ACL condition/modifier in \"%s\"", + saveline); + return NULL; + } + + /* The modifiers may not be negated */ + + if (negated && conditions[c].is_modifier) + { + *error = string_sprintf("ACL error: negation is not allowed with " + "\"%s\"", conditions[c].name); + return NULL; + } + + /* ENDPASS may occur only with ACCEPT or DISCARD. */ + + if (c == ACLC_ENDPASS && + this->verb != ACL_ACCEPT && + this->verb != ACL_DISCARD) + { + *error = string_sprintf("ACL error: \"%s\" is not allowed with \"%s\"", + conditions[c].name, verbs[this->verb]); + return NULL; + } + + cond = store_get(sizeof(acl_condition_block), GET_UNTAINTED); + cond->next = NULL; + cond->type = c; + cond->u.negated = negated; + + *condp = cond; + condp = &(cond->next); + + /* The "set" modifier is different in that its argument is "name=value" + rather than just a value, and we can check the validity of the name, which + gives us a variable name to insert into the data block. The original ACL + variable names were acl_c0 ... acl_c9 and acl_m0 ... acl_m9. This was + extended to 20 of each type, but after that people successfully argued for + arbitrary names. In the new scheme, the names must start with acl_c or acl_m. + After that, we allow alphanumerics and underscores, but the first character + after c or m must be a digit or an underscore. This retains backwards + compatibility. */ + + if (c == ACLC_SET) +#ifndef DISABLE_DKIM + if ( Ustrncmp(s, "dkim_verify_status", 18) == 0 + || Ustrncmp(s, "dkim_verify_reason", 18) == 0) + { + uschar * endptr = s+18; + + if (isalnum(*endptr)) + { + *error = string_sprintf("invalid variable name after \"set\" in ACL " + "modifier \"set %s\" " + "(only \"dkim_verify_status\" or \"dkim_verify_reason\" permitted)", + s); + return NULL; + } + cond->u.varname = string_copyn(s, 18); + s = endptr; + Uskip_whitespace(&s); + } + else +#endif + { + uschar *endptr; + + if (Ustrncmp(s, "acl_c", 5) != 0 && Ustrncmp(s, "acl_m", 5) != 0) + { + *error = string_sprintf("invalid variable name after \"set\" in ACL " + "modifier \"set %s\" (must start \"acl_c\" or \"acl_m\")", s); + return NULL; + } + + endptr = s + 5; + if (!isdigit(*endptr) && *endptr != '_') + { + *error = string_sprintf("invalid variable name after \"set\" in ACL " + "modifier \"set %s\" (digit or underscore must follow acl_c or acl_m)", + s); + return NULL; + } + + while (*endptr && *endptr != '=' && !isspace(*endptr)) + { + if (!isalnum(*endptr) && *endptr != '_') + { + *error = string_sprintf("invalid character \"%c\" in variable name " + "in ACL modifier \"set %s\"", *endptr, s); + return NULL; + } + endptr++; + } + + cond->u.varname = string_copyn(s + 4, endptr - s - 4); + s = endptr; + Uskip_whitespace(&s); + } + + /* For "set", we are now positioned for the data. For the others, only + "endpass" has no data */ + + if (c != ACLC_ENDPASS) + { + if (*s++ != '=') + { + *error = string_sprintf("\"=\" missing after ACL \"%s\" %s", name, + conditions[c].is_modifier ? US"modifier" : US"condition"); + return NULL; + } + Uskip_whitespace(&s); + cond->arg = string_copy(s); + } + } + +return yield; +} + + + +/************************************************* +* Set up added header line(s) * +*************************************************/ + +/* This function is called by the add_header modifier, and also from acl_warn() +to implement the now-deprecated way of adding header lines using "message" on a +"warn" verb. The argument is treated as a sequence of header lines which are +added to a chain, provided there isn't an identical one already there. + +Argument: string of header lines +Returns: nothing +*/ + +static void +setup_header(const uschar *hstring) +{ +const uschar *p, *q; +int hlen = Ustrlen(hstring); + +/* Ignore any leading newlines */ +while (*hstring == '\n') hstring++, hlen--; + +/* An empty string does nothing; ensure exactly one final newline. */ +if (hlen <= 0) return; +if (hstring[--hlen] != '\n') /* no newline */ + q = string_sprintf("%s\n", hstring); +else if (hstring[hlen-1] == '\n') /* double newline */ + { + uschar * s = string_copy(hstring); + while(s[--hlen] == '\n') + s[hlen+1] = '\0'; + q = s; + } +else + q = hstring; + +/* Loop for multiple header lines, taking care about continuations */ + +for (p = q; *p; p = q) + { + const uschar *s; + uschar * hdr; + int newtype = htype_add_bot; + header_line **hptr = &acl_added_headers; + + /* Find next header line within the string */ + + for (;;) + { + q = Ustrchr(q, '\n'); /* we know there was a newline */ + if (*++q != ' ' && *q != '\t') break; + } + + /* If the line starts with a colon, interpret the instruction for where to + add it. This temporarily sets up a new type. */ + + if (*p == ':') + { + if (strncmpic(p, US":after_received:", 16) == 0) + { + newtype = htype_add_rec; + p += 16; + } + else if (strncmpic(p, US":at_start_rfc:", 14) == 0) + { + newtype = htype_add_rfc; + p += 14; + } + else if (strncmpic(p, US":at_start:", 10) == 0) + { + newtype = htype_add_top; + p += 10; + } + else if (strncmpic(p, US":at_end:", 8) == 0) + { + newtype = htype_add_bot; + p += 8; + } + while (*p == ' ' || *p == '\t') p++; + } + + /* See if this line starts with a header name, and if not, add X-ACL-Warn: + to the front of it. */ + + for (s = p; s < q - 1; s++) + if (*s == ':' || !isgraph(*s)) break; + + hdr = string_sprintf("%s%.*s", *s == ':' ? "" : "X-ACL-Warn: ", (int) (q - p), p); + hlen = Ustrlen(hdr); + + /* See if this line has already been added */ + + while (*hptr) + { + if (Ustrncmp((*hptr)->text, hdr, hlen) == 0) break; + hptr = &(*hptr)->next; + } + + /* Add if not previously present */ + + if (!*hptr) + { + /* The header_line struct itself is not tainted, though it points to + possibly tainted data. */ + header_line * h = store_get(sizeof(header_line), GET_UNTAINTED); + h->text = hdr; + h->next = NULL; + h->type = newtype; + h->slen = hlen; + *hptr = h; + hptr = &h->next; + } + } +} + + + +/************************************************* +* List the added header lines * +*************************************************/ +uschar * +fn_hdrs_added(void) +{ +gstring * g = NULL; + +for (header_line * h = acl_added_headers; h; h = h->next) + { + int i = h->slen; + if (h->text[i-1] == '\n') i--; + g = string_append_listele_n(g, '\n', h->text, i); + } + +return g ? g->s : NULL; +} + + +/************************************************* +* Set up removed header line(s) * +*************************************************/ + +/* This function is called by the remove_header modifier. The argument is +treated as a sequence of header names which are added to a colon separated +list, provided there isn't an identical one already there. + +Argument: string of header names +Returns: nothing +*/ + +static void +setup_remove_header(const uschar *hnames) +{ +if (*hnames) + acl_removed_headers = acl_removed_headers + ? string_sprintf("%s : %s", acl_removed_headers, hnames) + : string_copy(hnames); +} + + + +/************************************************* +* Handle warnings * +*************************************************/ + +/* This function is called when a WARN verb's conditions are true. It adds to +the message's headers, and/or writes information to the log. In each case, this +only happens once (per message for headers, per connection for log). + +** NOTE: The header adding action using the "message" setting is historic, and +its use is now deprecated. The new add_header modifier should be used instead. + +Arguments: + where ACL_WHERE_xxxx indicating which ACL this is + user_message message for adding to headers + log_message message for logging, if different + +Returns: nothing +*/ + +static void +acl_warn(int where, uschar *user_message, uschar *log_message) +{ +if (log_message != NULL && log_message != user_message) + { + uschar *text; + string_item *logged; + + text = string_sprintf("%s Warning: %s", host_and_ident(TRUE), + string_printing(log_message)); + + /* If a sender verification has failed, and the log message is "sender verify + failed", add the failure message. */ + + if (sender_verified_failed != NULL && + sender_verified_failed->message != NULL && + strcmpic(log_message, US"sender verify failed") == 0) + text = string_sprintf("%s: %s", text, sender_verified_failed->message); + + /* Search previously logged warnings. They are kept in malloc + store so they can be freed at the start of a new message. */ + + for (logged = acl_warn_logged; logged; logged = logged->next) + if (Ustrcmp(logged->text, text) == 0) break; + + if (!logged) + { + int length = Ustrlen(text) + 1; + log_write(0, LOG_MAIN, "%s", text); + logged = store_malloc(sizeof(string_item) + length); + logged->text = US logged + sizeof(string_item); + memcpy(logged->text, text, length); + logged->next = acl_warn_logged; + acl_warn_logged = logged; + } + } + +/* If there's no user message, we are done. */ + +if (!user_message) return; + +/* If this isn't a message ACL, we can't do anything with a user message. +Log an error. */ + +if (where > ACL_WHERE_NOTSMTP) + { + log_write(0, LOG_MAIN|LOG_PANIC, "ACL \"warn\" with \"message\" setting " + "found in a non-message (%s) ACL: cannot specify header lines here: " + "message ignored", acl_wherenames[where]); + return; + } + +/* The code for setting up header lines is now abstracted into a separate +function so that it can be used for the add_header modifier as well. */ + +setup_header(user_message); +} + + + +/************************************************* +* Verify and check reverse DNS * +*************************************************/ + +/* Called from acl_verify() below. We look up the host name(s) of the client IP +address if this has not yet been done. The host_name_lookup() function checks +that one of these names resolves to an address list that contains the client IP +address, so we don't actually have to do the check here. + +Arguments: + user_msgptr pointer for user message + log_msgptr pointer for log message + +Returns: OK verification condition succeeded + FAIL verification failed + DEFER there was a problem verifying +*/ + +static int +acl_verify_reverse(uschar **user_msgptr, uschar **log_msgptr) +{ +int rc; + +/* Previous success */ + +if (sender_host_name) return OK; + +/* Previous failure */ + +if (host_lookup_failed) + { + *log_msgptr = string_sprintf("host lookup failed%s", host_lookup_msg); + return FAIL; + } + +/* Need to do a lookup */ + +HDEBUG(D_acl) + debug_printf_indent("looking up host name to force name/address consistency check\n"); + +if ((rc = host_name_lookup()) != OK) + { + *log_msgptr = rc == DEFER + ? US"host lookup deferred for reverse lookup check" + : string_sprintf("host lookup failed for reverse lookup check%s", + host_lookup_msg); + return rc; /* DEFER or FAIL */ + } + +host_build_sender_fullhost(); +return OK; +} + + + +/************************************************* +* Check client IP address matches CSA target * +*************************************************/ + +/* Called from acl_verify_csa() below. This routine scans a section of a DNS +response for address records belonging to the CSA target hostname. The section +is specified by the reset argument, either RESET_ADDITIONAL or RESET_ANSWERS. +If one of the addresses matches the client's IP address, then the client is +authorized by CSA. If there are target IP addresses but none of them match +then the client is using an unauthorized IP address. If there are no target IP +addresses then the client cannot be using an authorized IP address. (This is +an odd configuration - why didn't the SRV record have a weight of 1 instead?) + +Arguments: + dnsa the DNS answer block + dnss a DNS scan block for us to use + reset option specifying what portion to scan, as described above + target the target hostname to use for matching RR names + +Returns: CSA_OK successfully authorized + CSA_FAIL_MISMATCH addresses found but none matched + CSA_FAIL_NOADDR no target addresses found +*/ + +static int +acl_verify_csa_address(dns_answer *dnsa, dns_scan *dnss, int reset, + uschar *target) +{ +int rc = CSA_FAIL_NOADDR; + +for (dns_record * rr = dns_next_rr(dnsa, dnss, reset); + rr; + rr = dns_next_rr(dnsa, dnss, RESET_NEXT)) + { + /* Check this is an address RR for the target hostname. */ + + if (rr->type != T_A + #if HAVE_IPV6 + && rr->type != T_AAAA + #endif + ) continue; + + if (strcmpic(target, rr->name) != 0) continue; + + rc = CSA_FAIL_MISMATCH; + + /* Turn the target address RR into a list of textual IP addresses and scan + the list. There may be more than one if it is an A6 RR. */ + + for (dns_address * da = dns_address_from_rr(dnsa, rr); da; da = da->next) + { + /* If the client IP address matches the target IP address, it's good! */ + + DEBUG(D_acl) debug_printf_indent("CSA target address is %s\n", da->address); + + if (strcmpic(sender_host_address, da->address) == 0) return CSA_OK; + } + } + +/* If we found some target addresses but none of them matched, the client is +using an unauthorized IP address, otherwise the target has no authorized IP +addresses. */ + +return rc; +} + + + +/************************************************* +* Verify Client SMTP Authorization * +*************************************************/ + +/* Called from acl_verify() below. This routine calls dns_lookup_special() +to find the CSA SRV record corresponding to the domain argument, or +$sender_helo_name if no argument is provided. It then checks that the +client is authorized, and that its IP address corresponds to the SRV +target's address by calling acl_verify_csa_address() above. The address +should have been returned in the DNS response's ADDITIONAL section, but if +not we perform another DNS lookup to get it. + +Arguments: + domain pointer to optional parameter following verify = csa + +Returns: CSA_UNKNOWN no valid CSA record found + CSA_OK successfully authorized + CSA_FAIL_* client is definitely not authorized + CSA_DEFER_* there was a DNS problem +*/ + +static int +acl_verify_csa(const uschar *domain) +{ +tree_node *t; +const uschar *found; +int priority, weight, port; +dns_answer * dnsa; +dns_scan dnss; +dns_record *rr; +int rc, type, yield; +#define TARGET_SIZE 256 +uschar * target = store_get(TARGET_SIZE, GET_TAINTED); + +/* Work out the domain we are using for the CSA lookup. The default is the +client's HELO domain. If the client has not said HELO, use its IP address +instead. If it's a local client (exim -bs), CSA isn't applicable. */ + +while (isspace(*domain) && *domain != '\0') ++domain; +if (*domain == '\0') domain = sender_helo_name; +if (!domain) domain = sender_host_address; +if (!sender_host_address) return CSA_UNKNOWN; + +/* If we have an address literal, strip off the framing ready for turning it +into a domain. The framing consists of matched square brackets possibly +containing a keyword and a colon before the actual IP address. */ + +if (domain[0] == '[') + { + const uschar *start = Ustrchr(domain, ':'); + if (start == NULL) start = domain; + domain = string_copyn(start + 1, Ustrlen(start) - 2); + } + +/* Turn domains that look like bare IP addresses into domains in the reverse +DNS. This code also deals with address literals and $sender_host_address. It's +not quite kosher to treat bare domains such as EHLO 192.0.2.57 the same as +address literals, but it's probably the most friendly thing to do. This is an +extension to CSA, so we allow it to be turned off for proper conformance. */ + +if (string_is_ip_address(domain, NULL) != 0) + { + if (!dns_csa_use_reverse) return CSA_UNKNOWN; + domain = dns_build_reverse(domain); + } + +/* Find out if we've already done the CSA check for this domain. If we have, +return the same result again. Otherwise build a new cached result structure +for this domain. The name is filled in now, and the value is filled in when +we return from this function. */ + +if ((t = tree_search(csa_cache, domain))) + return t->data.val; + +t = store_get_perm(sizeof(tree_node) + Ustrlen(domain), domain); +Ustrcpy(t->name, domain); +(void)tree_insertnode(&csa_cache, t); + +/* Now we are ready to do the actual DNS lookup(s). */ + +found = domain; +dnsa = store_get_dns_answer(); +switch (dns_special_lookup(dnsa, domain, T_CSA, &found)) + { + /* If something bad happened (most commonly DNS_AGAIN), defer. */ + + default: + yield = CSA_DEFER_SRV; + goto out; + + /* If we found nothing, the client's authorization is unknown. */ + + case DNS_NOMATCH: + case DNS_NODATA: + yield = CSA_UNKNOWN; + goto out; + + /* We got something! Go on to look at the reply in more detail. */ + + case DNS_SUCCEED: + break; + } + +/* Scan the reply for well-formed CSA SRV records. */ + +for (rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); + rr; + rr = dns_next_rr(dnsa, &dnss, RESET_NEXT)) if (rr->type == T_SRV) + { + const uschar * p = rr->data; + + /* Extract the numerical SRV fields (p is incremented) */ + + GETSHORT(priority, p); + GETSHORT(weight, p); + GETSHORT(port, p); + + DEBUG(D_acl) + debug_printf_indent("CSA priority=%d weight=%d port=%d\n", priority, weight, port); + + /* Check the CSA version number */ + + if (priority != 1) continue; + + /* If the domain does not have a CSA SRV record of its own (i.e. the domain + found by dns_special_lookup() is a parent of the one we asked for), we check + the subdomain assertions in the port field. At the moment there's only one + assertion: legitimate SMTP clients are all explicitly authorized with CSA + SRV records of their own. */ + + if (Ustrcmp(found, domain) != 0) + { + yield = port & 1 ? CSA_FAIL_EXPLICIT : CSA_UNKNOWN; + goto out; + } + + /* This CSA SRV record refers directly to our domain, so we check the value + in the weight field to work out the domain's authorization. 0 and 1 are + unauthorized; 3 means the client is authorized but we can't check the IP + address in order to authenticate it, so we treat it as unknown; values + greater than 3 are undefined. */ + + if (weight < 2) + { + yield = CSA_FAIL_DOMAIN; + goto out; + } + + if (weight > 2) continue; + + /* Weight == 2, which means the domain is authorized. We must check that the + client's IP address is listed as one of the SRV target addresses. Save the + target hostname then break to scan the additional data for its addresses. */ + + (void)dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen, p, + (DN_EXPAND_ARG4_TYPE)target, TARGET_SIZE); + + DEBUG(D_acl) debug_printf_indent("CSA target is %s\n", target); + + break; + } + +/* If we didn't break the loop then no appropriate records were found. */ + +if (!rr) + { + yield = CSA_UNKNOWN; + goto out; + } + +/* Do not check addresses if the target is ".", in accordance with RFC 2782. +A target of "." indicates there are no valid addresses, so the client cannot +be authorized. (This is an odd configuration because weight=2 target=. is +equivalent to weight=1, but we check for it in order to keep load off the +root name servers.) Note that dn_expand() turns "." into "". */ + +if (Ustrcmp(target, "") == 0) + { + yield = CSA_FAIL_NOADDR; + goto out; + } + +/* Scan the additional section of the CSA SRV reply for addresses belonging +to the target. If the name server didn't return any additional data (e.g. +because it does not fully support SRV records), we need to do another lookup +to obtain the target addresses; otherwise we have a definitive result. */ + +rc = acl_verify_csa_address(dnsa, &dnss, RESET_ADDITIONAL, target); +if (rc != CSA_FAIL_NOADDR) + { + yield = rc; + goto out; + } + +/* The DNS lookup type corresponds to the IP version used by the client. */ + +#if HAVE_IPV6 +if (Ustrchr(sender_host_address, ':') != NULL) + type = T_AAAA; +else +#endif /* HAVE_IPV6 */ + type = T_A; + + +lookup_dnssec_authenticated = NULL; +switch (dns_lookup(dnsa, target, type, NULL)) + { + /* If something bad happened (most commonly DNS_AGAIN), defer. */ + + default: + yield = CSA_DEFER_ADDR; + break; + + /* If the query succeeded, scan the addresses and return the result. */ + + case DNS_SUCCEED: + rc = acl_verify_csa_address(dnsa, &dnss, RESET_ANSWERS, target); + if (rc != CSA_FAIL_NOADDR) + { + yield = rc; + break; + } + /* else fall through */ + + /* If the target has no IP addresses, the client cannot have an authorized + IP address. However, if the target site uses A6 records (not AAAA records) + we have to do yet another lookup in order to check them. */ + + case DNS_NOMATCH: + case DNS_NODATA: + yield = CSA_FAIL_NOADDR; + break; + } + +out: + +store_free_dns_answer(dnsa); +return t->data.val = yield; +} + + + +/************************************************* +* Handle verification (address & other) * +*************************************************/ + +enum { VERIFY_REV_HOST_LKUP, VERIFY_CERT, VERIFY_HELO, VERIFY_CSA, VERIFY_HDR_SYNTAX, + VERIFY_NOT_BLIND, VERIFY_HDR_SNDR, VERIFY_SNDR, VERIFY_RCPT, + VERIFY_HDR_NAMES_ASCII, VERIFY_ARC + }; +typedef struct { + uschar * name; + int value; + unsigned where_allowed; /* bitmap */ + BOOL no_options; /* Never has /option(s) following */ + unsigned alt_opt_sep; /* >0 Non-/ option separator (custom parser) */ + } verify_type_t; +static verify_type_t verify_type_list[] = { + /* name value where no-opt opt-sep */ + { US"reverse_host_lookup", VERIFY_REV_HOST_LKUP, (unsigned)~0, FALSE, 0 }, + { US"certificate", VERIFY_CERT, (unsigned)~0, TRUE, 0 }, + { US"helo", VERIFY_HELO, (unsigned)~0, TRUE, 0 }, + { US"csa", VERIFY_CSA, (unsigned)~0, FALSE, 0 }, + { US"header_syntax", VERIFY_HDR_SYNTAX, ACL_BITS_HAVEDATA, TRUE, 0 }, + { US"not_blind", VERIFY_NOT_BLIND, ACL_BITS_HAVEDATA, FALSE, 0 }, + { US"header_sender", VERIFY_HDR_SNDR, ACL_BITS_HAVEDATA, FALSE, 0 }, + { US"sender", VERIFY_SNDR, ACL_BIT_MAIL | ACL_BIT_RCPT + | ACL_BIT_PREDATA | ACL_BIT_DATA | ACL_BIT_NOTSMTP, + FALSE, 6 }, + { US"recipient", VERIFY_RCPT, ACL_BIT_RCPT, FALSE, 0 }, + { US"header_names_ascii", VERIFY_HDR_NAMES_ASCII, ACL_BITS_HAVEDATA, TRUE, 0 }, +#ifdef EXPERIMENTAL_ARC + { US"arc", VERIFY_ARC, ACL_BIT_DATA, FALSE , 0 }, +#endif + }; + + +enum { CALLOUT_DEFER_OK, CALLOUT_NOCACHE, CALLOUT_RANDOM, CALLOUT_USE_SENDER, + CALLOUT_USE_POSTMASTER, CALLOUT_POSTMASTER, CALLOUT_FULLPOSTMASTER, + CALLOUT_MAILFROM, CALLOUT_POSTMASTER_MAILFROM, CALLOUT_MAXWAIT, CALLOUT_CONNECT, + CALLOUT_HOLD, CALLOUT_TIME /* TIME must be last */ + }; +typedef struct { + uschar * name; + int value; + int flag; + BOOL has_option; /* Has =option(s) following */ + BOOL timeval; /* Has a time value */ + } callout_opt_t; +static callout_opt_t callout_opt_list[] = { + /* name value flag has-opt has-time */ + { US"defer_ok", CALLOUT_DEFER_OK, 0, FALSE, FALSE }, + { US"no_cache", CALLOUT_NOCACHE, vopt_callout_no_cache, FALSE, FALSE }, + { US"random", CALLOUT_RANDOM, vopt_callout_random, FALSE, FALSE }, + { US"use_sender", CALLOUT_USE_SENDER, vopt_callout_recipsender, FALSE, FALSE }, + { US"use_postmaster", CALLOUT_USE_POSTMASTER,vopt_callout_recippmaster, FALSE, FALSE }, + { US"postmaster_mailfrom",CALLOUT_POSTMASTER_MAILFROM,0, TRUE, FALSE }, + { US"postmaster", CALLOUT_POSTMASTER, 0, FALSE, FALSE }, + { US"fullpostmaster", CALLOUT_FULLPOSTMASTER,vopt_callout_fullpm, FALSE, FALSE }, + { US"mailfrom", CALLOUT_MAILFROM, 0, TRUE, FALSE }, + { US"maxwait", CALLOUT_MAXWAIT, 0, TRUE, TRUE }, + { US"connect", CALLOUT_CONNECT, 0, TRUE, TRUE }, + { US"hold", CALLOUT_HOLD, vopt_callout_hold, FALSE, FALSE }, + { NULL, CALLOUT_TIME, 0, FALSE, TRUE } + }; + + + +static int +v_period(const uschar * s, const uschar * arg, uschar ** log_msgptr) +{ +int period; +if ((period = readconf_readtime(s, 0, FALSE)) < 0) + { + *log_msgptr = string_sprintf("bad time value in ACL condition " + "\"verify %s\"", arg); + } +return period; +} + + + +/* This function implements the "verify" condition. It is called when +encountered in any ACL, because some tests are almost always permitted. Some +just don't make sense, and always fail (for example, an attempt to test a host +lookup for a non-TCP/IP message). Others are restricted to certain ACLs. + +Arguments: + where where called from + addr the recipient address that the ACL is handling, or NULL + arg the argument of "verify" + user_msgptr pointer for user message + log_msgptr pointer for log message + basic_errno where to put verify errno + +Returns: OK verification condition succeeded + FAIL verification failed + DEFER there was a problem verifying + ERROR syntax error +*/ + +static int +acl_verify(int where, address_item *addr, const uschar *arg, + uschar **user_msgptr, uschar **log_msgptr, int *basic_errno) +{ +int sep = '/'; +int callout = -1; +int callout_overall = -1; +int callout_connect = -1; +int verify_options = 0; +int rc; +BOOL verify_header_sender = FALSE; +BOOL defer_ok = FALSE; +BOOL callout_defer_ok = FALSE; +BOOL no_details = FALSE; +BOOL success_on_redirect = FALSE; +BOOL quota = FALSE; +int quota_pos_cache = QUOTA_POS_DEFAULT, quota_neg_cache = QUOTA_NEG_DEFAULT; +address_item *sender_vaddr = NULL; +uschar *verify_sender_address = NULL; +uschar *pm_mailfrom = NULL; +uschar *se_mailfrom = NULL; + +/* Some of the verify items have slash-separated options; some do not. Diagnose +an error if options are given for items that don't expect them. +*/ + +uschar *slash = Ustrchr(arg, '/'); +const uschar *list = arg; +uschar *ss = string_nextinlist(&list, &sep, NULL, 0); +verify_type_t * vp; + +if (!ss) goto BAD_VERIFY; + +/* Handle name/address consistency verification in a separate function. */ + +for (vp = verify_type_list; + CS vp < CS verify_type_list + sizeof(verify_type_list); + vp++ + ) + if (vp->alt_opt_sep ? strncmpic(ss, vp->name, vp->alt_opt_sep) == 0 + : strcmpic (ss, vp->name) == 0) + break; +if (CS vp >= CS verify_type_list + sizeof(verify_type_list)) + goto BAD_VERIFY; + +if (vp->no_options && slash) + { + *log_msgptr = string_sprintf("unexpected '/' found in \"%s\" " + "(this verify item has no options)", arg); + return ERROR; + } +if (!(vp->where_allowed & BIT(where))) + { + *log_msgptr = string_sprintf("cannot verify %s in ACL for %s", + vp->name, acl_wherenames[where]); + return ERROR; + } +switch(vp->value) + { + case VERIFY_REV_HOST_LKUP: + if (!sender_host_address) return OK; + if ((rc = acl_verify_reverse(user_msgptr, log_msgptr)) == DEFER) + while ((ss = string_nextinlist(&list, &sep, NULL, 0))) + if (strcmpic(ss, US"defer_ok") == 0) + return OK; + return rc; + + case VERIFY_CERT: + /* TLS certificate verification is done at STARTTLS time; here we just + test whether it was successful or not. (This is for optional verification; for + mandatory verification, the connection doesn't last this long.) */ + + if (tls_in.certificate_verified) return OK; + *user_msgptr = US"no verified certificate"; + return FAIL; + + case VERIFY_HELO: + /* We can test the result of optional HELO verification that might have + occurred earlier. If not, we can attempt the verification now. */ + + if (!f.helo_verified && !f.helo_verify_failed) smtp_verify_helo(); + return f.helo_verified ? OK : FAIL; + + case VERIFY_CSA: + /* Do Client SMTP Authorization checks in a separate function, and turn the + result code into user-friendly strings. */ + + rc = acl_verify_csa(list); + *log_msgptr = *user_msgptr = string_sprintf("client SMTP authorization %s", + csa_reason_string[rc]); + csa_status = csa_status_string[rc]; + DEBUG(D_acl) debug_printf_indent("CSA result %s\n", csa_status); + return csa_return_code[rc]; + +#ifdef EXPERIMENTAL_ARC + case VERIFY_ARC: + { /* Do Authenticated Received Chain checks in a separate function. */ + const uschar * condlist = CUS string_nextinlist(&list, &sep, NULL, 0); + int csep = 0; + uschar * cond; + + if (!(arc_state = acl_verify_arc())) return DEFER; + DEBUG(D_acl) debug_printf_indent("ARC verify result %s %s%s%s\n", arc_state, + arc_state_reason ? "(":"", arc_state_reason, arc_state_reason ? ")":""); + + if (!condlist) condlist = US"none:pass"; + while ((cond = string_nextinlist(&condlist, &csep, NULL, 0))) + if (Ustrcmp(arc_state, cond) == 0) return OK; + return FAIL; + } +#endif + + case VERIFY_HDR_SYNTAX: + /* Check that all relevant header lines have the correct 5322-syntax. If there is + a syntax error, we return details of the error to the sender if configured to + send out full details. (But a "message" setting on the ACL can override, as + always). */ + + rc = verify_check_headers(log_msgptr); + if (rc != OK && *log_msgptr) + if (smtp_return_error_details) + *user_msgptr = string_sprintf("Rejected after DATA: %s", *log_msgptr); + else + acl_verify_message = *log_msgptr; + return rc; + + case VERIFY_HDR_NAMES_ASCII: + /* Check that all header names are true 7 bit strings + See RFC 5322, 2.2. and RFC 6532, 3. */ + + rc = verify_check_header_names_ascii(log_msgptr); + if (rc != OK && smtp_return_error_details && *log_msgptr) + *user_msgptr = string_sprintf("Rejected after DATA: %s", *log_msgptr); + return rc; + + case VERIFY_NOT_BLIND: + /* Check that no recipient of this message is "blind", that is, every envelope + recipient must be mentioned in either To: or Cc:. */ + { + BOOL case_sensitive = TRUE; + + while ((ss = string_nextinlist(&list, &sep, NULL, 0))) + if (strcmpic(ss, US"case_insensitive") == 0) + case_sensitive = FALSE; + else + { + *log_msgptr = string_sprintf("unknown option \"%s\" in ACL " + "condition \"verify %s\"", ss, arg); + return ERROR; + } + + if ((rc = verify_check_notblind(case_sensitive)) != OK) + { + *log_msgptr = US"bcc recipient detected"; + if (smtp_return_error_details) + *user_msgptr = string_sprintf("Rejected after DATA: %s", *log_msgptr); + } + return rc; + } + + /* The remaining verification tests check recipient and sender addresses, + either from the envelope or from the header. There are a number of + slash-separated options that are common to all of them. */ + + case VERIFY_HDR_SNDR: + verify_header_sender = TRUE; + break; + + case VERIFY_SNDR: + /* In the case of a sender, this can optionally be followed by an address to use + in place of the actual sender (rare special-case requirement). */ + { + uschar *s = ss + 6; + if (!*s) + verify_sender_address = sender_address; + else + { + while (isspace(*s)) s++; + if (*s++ != '=') goto BAD_VERIFY; + while (isspace(*s)) s++; + verify_sender_address = string_copy(s); + } + } + break; + + case VERIFY_RCPT: + break; + } + + + +/* Remaining items are optional; they apply to sender and recipient +verification, including "header sender" verification. */ + +while ((ss = string_nextinlist(&list, &sep, NULL, 0))) + { + if (strcmpic(ss, US"defer_ok") == 0) defer_ok = TRUE; + else if (strcmpic(ss, US"no_details") == 0) no_details = TRUE; + else if (strcmpic(ss, US"success_on_redirect") == 0) success_on_redirect = TRUE; + + /* These two old options are left for backwards compatibility */ + + else if (strcmpic(ss, US"callout_defer_ok") == 0) + { + callout_defer_ok = TRUE; + if (callout == -1) callout = CALLOUT_TIMEOUT_DEFAULT; + } + + else if (strcmpic(ss, US"check_postmaster") == 0) + { + pm_mailfrom = US""; + if (callout == -1) callout = CALLOUT_TIMEOUT_DEFAULT; + } + + /* The callout option has a number of sub-options, comma separated */ + + else if (strncmpic(ss, US"callout", 7) == 0) + { + callout = CALLOUT_TIMEOUT_DEFAULT; + if (*(ss += 7)) + { + while (isspace(*ss)) ss++; + if (*ss++ == '=') + { + const uschar * sublist = ss; + int optsep = ','; + + while (isspace(*sublist)) sublist++; + for (uschar * opt; opt = string_nextinlist(&sublist, &optsep, NULL, 0); ) + { + callout_opt_t * op; + double period = 1.0F; + + for (op= callout_opt_list; op->name; op++) + if (strncmpic(opt, op->name, Ustrlen(op->name)) == 0) + break; + + verify_options |= op->flag; + if (op->has_option) + { + opt += Ustrlen(op->name); + while (isspace(*opt)) opt++; + if (*opt++ != '=') + { + *log_msgptr = string_sprintf("'=' expected after " + "\"%s\" in ACL verify condition \"%s\"", op->name, arg); + return ERROR; + } + while (isspace(*opt)) opt++; + } + if (op->timeval && (period = v_period(opt, arg, log_msgptr)) < 0) + return ERROR; + + switch(op->value) + { + case CALLOUT_DEFER_OK: callout_defer_ok = TRUE; break; + case CALLOUT_POSTMASTER: pm_mailfrom = US""; break; + case CALLOUT_FULLPOSTMASTER: pm_mailfrom = US""; break; + case CALLOUT_MAILFROM: + if (!verify_header_sender) + { + *log_msgptr = string_sprintf("\"mailfrom\" is allowed as a " + "callout option only for verify=header_sender (detected in ACL " + "condition \"%s\")", arg); + return ERROR; + } + se_mailfrom = string_copy(opt); + break; + case CALLOUT_POSTMASTER_MAILFROM: pm_mailfrom = string_copy(opt); break; + case CALLOUT_MAXWAIT: callout_overall = period; break; + case CALLOUT_CONNECT: callout_connect = period; break; + case CALLOUT_TIME: callout = period; break; + } + } + } + else + { + *log_msgptr = string_sprintf("'=' expected after \"callout\" in " + "ACL condition \"%s\"", arg); + return ERROR; + } + } + } + + /* The quota option has sub-options, comma-separated */ + + else if (strncmpic(ss, US"quota", 5) == 0) + { + quota = TRUE; + if (*(ss += 5)) + { + while (isspace(*ss)) ss++; + if (*ss++ == '=') + { + const uschar * sublist = ss; + int optsep = ','; + int period; + + while (isspace(*sublist)) sublist++; + for (uschar * opt; opt = string_nextinlist(&sublist, &optsep, NULL, 0); ) + if (Ustrncmp(opt, "cachepos=", 9) == 0) + if ((period = v_period(opt += 9, arg, log_msgptr)) < 0) + return ERROR; + else + quota_pos_cache = period; + else if (Ustrncmp(opt, "cacheneg=", 9) == 0) + if ((period = v_period(opt += 9, arg, log_msgptr)) < 0) + return ERROR; + else + quota_neg_cache = period; + else if (Ustrcmp(opt, "no_cache") == 0) + quota_pos_cache = quota_neg_cache = 0; + } + } + } + + /* Option not recognized */ + + else + { + *log_msgptr = string_sprintf("unknown option \"%s\" in ACL " + "condition \"verify %s\"", ss, arg); + return ERROR; + } + } + +if ((verify_options & (vopt_callout_recipsender|vopt_callout_recippmaster)) == + (vopt_callout_recipsender|vopt_callout_recippmaster)) + { + *log_msgptr = US"only one of use_sender and use_postmaster can be set " + "for a recipient callout"; + return ERROR; + } + +/* Handle quota verification */ +if (quota) + { + if (vp->value != VERIFY_RCPT) + { + *log_msgptr = US"can only verify quota of recipient"; + return ERROR; + } + + if ((rc = verify_quota_call(addr->address, + quota_pos_cache, quota_neg_cache, log_msgptr)) != OK) + { + *basic_errno = errno; + if (smtp_return_error_details) + { + if (!*user_msgptr && *log_msgptr) + *user_msgptr = string_sprintf("Rejected after %s: %s", + smtp_names[smtp_connection_had[SMTP_HBUFF_PREV(smtp_ch_index)]], + *log_msgptr); + if (rc == DEFER) f.acl_temp_details = TRUE; + } + } + + return rc; + } + +/* Handle sender-in-header verification. Default the user message to the log +message if giving out verification details. */ + +if (verify_header_sender) + { + int verrno; + + if ((rc = verify_check_header_address(user_msgptr, log_msgptr, callout, + callout_overall, callout_connect, se_mailfrom, pm_mailfrom, verify_options, + &verrno)) != OK) + { + *basic_errno = verrno; + if (smtp_return_error_details) + { + if (!*user_msgptr && *log_msgptr) + *user_msgptr = string_sprintf("Rejected after DATA: %s", *log_msgptr); + if (rc == DEFER) f.acl_temp_details = TRUE; + } + } + } + +/* Handle a sender address. The default is to verify *the* sender address, but +optionally a different address can be given, for special requirements. If the +address is empty, we are dealing with a bounce message that has no sender, so +we cannot do any checking. If the real sender address gets rewritten during +verification (e.g. DNS widening), set the flag to stop it being rewritten again +during message reception. + +A list of verified "sender" addresses is kept to try to avoid doing to much +work repetitively when there are multiple recipients in a message and they all +require sender verification. However, when callouts are involved, it gets too +complicated because different recipients may require different callout options. +Therefore, we always do a full sender verify when any kind of callout is +specified. Caching elsewhere, for instance in the DNS resolver and in the +callout handling, should ensure that this is not terribly inefficient. */ + +else if (verify_sender_address) + { + if ((verify_options & (vopt_callout_recipsender|vopt_callout_recippmaster))) + { + *log_msgptr = US"use_sender or use_postmaster cannot be used for a " + "sender verify callout"; + return ERROR; + } + + sender_vaddr = verify_checked_sender(verify_sender_address); + if ( sender_vaddr /* Previously checked */ + && callout <= 0) /* No callout needed this time */ + { + /* If the "routed" flag is set, it means that routing worked before, so + this check can give OK (the saved return code value, if set, belongs to a + callout that was done previously). If the "routed" flag is not set, routing + must have failed, so we use the saved return code. */ + + if (testflag(sender_vaddr, af_verify_routed)) + rc = OK; + else + { + rc = sender_vaddr->special_action; + *basic_errno = sender_vaddr->basic_errno; + } + HDEBUG(D_acl) debug_printf_indent("using cached sender verify result\n"); + } + + /* Do a new verification, and cache the result. The cache is used to avoid + verifying the sender multiple times for multiple RCPTs when callouts are not + specified (see comments above). + + The cache is also used on failure to give details in response to the first + RCPT that gets bounced for this reason. However, this can be suppressed by + the no_details option, which sets the flag that says "this detail has already + been sent". The cache normally contains just one address, but there may be + more in esoteric circumstances. */ + + else + { + BOOL routed = TRUE; + uschar *save_address_data = deliver_address_data; + + sender_vaddr = deliver_make_addr(verify_sender_address, TRUE); +#ifdef SUPPORT_I18N + if ((sender_vaddr->prop.utf8_msg = message_smtputf8)) + { + sender_vaddr->prop.utf8_downcvt = message_utf8_downconvert == 1; + sender_vaddr->prop.utf8_downcvt_maybe = message_utf8_downconvert == -1; + } +#endif + if (no_details) setflag(sender_vaddr, af_sverify_told); + if (verify_sender_address[0] != 0) + { + /* If this is the real sender address, save the unrewritten version + for use later in receive. Otherwise, set a flag so that rewriting the + sender in verify_address() does not update sender_address. */ + + if (verify_sender_address == sender_address) + sender_address_unrewritten = sender_address; + else + verify_options |= vopt_fake_sender; + + if (success_on_redirect) + verify_options |= vopt_success_on_redirect; + + /* The recipient, qualify, and expn options are never set in + verify_options. */ + + rc = verify_address(sender_vaddr, NULL, verify_options, callout, + callout_overall, callout_connect, se_mailfrom, pm_mailfrom, &routed); + + HDEBUG(D_acl) debug_printf_indent("----------- end verify ------------\n"); + + if (rc != OK) + *basic_errno = sender_vaddr->basic_errno; + else + DEBUG(D_acl) + if (Ustrcmp(sender_vaddr->address, verify_sender_address) != 0) + debug_printf_indent("sender %s verified ok as %s\n", + verify_sender_address, sender_vaddr->address); + else + debug_printf_indent("sender %s verified ok\n", + verify_sender_address); + } + else + rc = OK; /* Null sender */ + + /* Cache the result code */ + + if (routed) setflag(sender_vaddr, af_verify_routed); + if (callout > 0) setflag(sender_vaddr, af_verify_callout); + sender_vaddr->special_action = rc; + sender_vaddr->next = sender_verified_list; + sender_verified_list = sender_vaddr; + + /* Restore the recipient address data, which might have been clobbered by + the sender verification. */ + + deliver_address_data = save_address_data; + } + + /* Put the sender address_data value into $sender_address_data */ + + sender_address_data = sender_vaddr->prop.address_data; + } + +/* A recipient address just gets a straightforward verify; again we must handle +the DEFER overrides. */ + +else + { + address_item addr2; + + if (success_on_redirect) + verify_options |= vopt_success_on_redirect; + + /* We must use a copy of the address for verification, because it might + get rewritten. */ + + addr2 = *addr; + rc = verify_address(&addr2, NULL, verify_options|vopt_is_recipient, callout, + callout_overall, callout_connect, se_mailfrom, pm_mailfrom, NULL); + HDEBUG(D_acl) debug_printf_indent("----------- end verify ------------\n"); + + *basic_errno = addr2.basic_errno; + *log_msgptr = addr2.message; + *user_msgptr = addr2.user_message ? addr2.user_message : addr2.message; + + /* Allow details for temporary error if the address is so flagged. */ + if (testflag((&addr2), af_pass_message)) f.acl_temp_details = TRUE; + + /* Make $address_data visible */ + deliver_address_data = addr2.prop.address_data; + } + +/* We have a result from the relevant test. Handle defer overrides first. */ + +if ( rc == DEFER + && ( defer_ok + || callout_defer_ok && *basic_errno == ERRNO_CALLOUTDEFER + ) ) + { + HDEBUG(D_acl) debug_printf_indent("verify defer overridden by %s\n", + defer_ok? "defer_ok" : "callout_defer_ok"); + rc = OK; + } + +/* If we've failed a sender, set up a recipient message, and point +sender_verified_failed to the address item that actually failed. */ + +if (rc != OK && verify_sender_address) + { + if (rc != DEFER) + *log_msgptr = *user_msgptr = US"Sender verify failed"; + else if (*basic_errno != ERRNO_CALLOUTDEFER) + *log_msgptr = *user_msgptr = US"Could not complete sender verify"; + else + { + *log_msgptr = US"Could not complete sender verify callout"; + *user_msgptr = smtp_return_error_details? sender_vaddr->user_message : + *log_msgptr; + } + + sender_verified_failed = sender_vaddr; + } + +/* Verifying an address messes up the values of $domain and $local_part, +so reset them before returning if this is a RCPT ACL. */ + +if (addr) + { + deliver_domain = addr->domain; + deliver_localpart = addr->local_part; + } +return rc; + +/* Syntax errors in the verify argument come here. */ + +BAD_VERIFY: +*log_msgptr = string_sprintf("expected \"sender[=address]\", \"recipient\", " + "\"helo\", \"header_syntax\", \"header_sender\", \"header_names_ascii\" " + "or \"reverse_host_lookup\" at start of ACL condition " + "\"verify %s\"", arg); +return ERROR; +} + + + + +/************************************************* +* Check argument for control= modifier * +*************************************************/ + +/* Called from acl_check_condition() below. +To handle the case "queue_only" we accept an _ in the +initial / option-switch position. + +Arguments: + arg the argument string for control= + pptr set to point to the terminating character + where which ACL we are in + log_msgptr for error messages + +Returns: CONTROL_xxx value +*/ + +static int +decode_control(const uschar *arg, const uschar **pptr, int where, uschar **log_msgptr) +{ +int idx, len; +control_def * d; +uschar c; + +if ( (idx = find_control(arg, controls_list, nelem(controls_list))) < 0 + || ( (c = arg[len = Ustrlen((d = controls_list+idx)->name)]) != 0 + && (!d->has_option || c != '/' && c != '_') + ) ) + { + *log_msgptr = string_sprintf("syntax error in \"control=%s\"", arg); + return CONTROL_ERROR; + } + +*pptr = arg + len; +return idx; +} + + + + +/************************************************* +* Return a ratelimit error * +*************************************************/ + +/* Called from acl_ratelimit() below + +Arguments: + log_msgptr for error messages + format format string + ... supplementary arguments + +Returns: ERROR +*/ + +static int +ratelimit_error(uschar **log_msgptr, const char *format, ...) +{ +va_list ap; +gstring * g = + string_cat(NULL, US"error in arguments to \"ratelimit\" condition: "); + +va_start(ap, format); +g = string_vformat(g, SVFMT_EXTEND|SVFMT_REBUFFER, format, ap); +va_end(ap); + +gstring_release_unused(g); +*log_msgptr = string_from_gstring(g); +return ERROR; +} + + + + +/************************************************* +* Handle rate limiting * +*************************************************/ + +/* Called by acl_check_condition() below to calculate the result +of the ACL ratelimit condition. + +Note that the return value might be slightly unexpected: if the +sender's rate is above the limit then the result is OK. This is +similar to the dnslists condition, and is so that you can write +ACL clauses like: defer ratelimit = 15 / 1h + +Arguments: + arg the option string for ratelimit= + where ACL_WHERE_xxxx indicating which ACL this is + log_msgptr for error messages + +Returns: OK - Sender's rate is above limit + FAIL - Sender's rate is below limit + DEFER - Problem opening ratelimit database + ERROR - Syntax error in options. +*/ + +static int +acl_ratelimit(const uschar *arg, int where, uschar **log_msgptr) +{ +double limit, period, count; +uschar *ss; +uschar *key = NULL; +uschar *unique = NULL; +int sep = '/'; +BOOL leaky = FALSE, strict = FALSE, readonly = FALSE; +BOOL noupdate = FALSE, badacl = FALSE; +int mode = RATE_PER_WHAT; +int old_pool, rc; +tree_node **anchor, *t; +open_db dbblock, *dbm; +int dbdb_size; +dbdata_ratelimit *dbd; +dbdata_ratelimit_unique *dbdb; +struct timeval tv; + +/* Parse the first two options and record their values in expansion +variables. These variables allow the configuration to have informative +error messages based on rate limits obtained from a table lookup. */ + +/* First is the maximum number of messages per period / maximum burst +size, which must be greater than or equal to zero. Zero is useful for +rate measurement as opposed to rate limiting. */ + +if (!(sender_rate_limit = string_nextinlist(&arg, &sep, NULL, 0))) + return ratelimit_error(log_msgptr, "sender rate limit not set"); + +limit = Ustrtod(sender_rate_limit, &ss); +if (tolower(*ss) == 'k') { limit *= 1024.0; ss++; } +else if (tolower(*ss) == 'm') { limit *= 1024.0*1024.0; ss++; } +else if (tolower(*ss) == 'g') { limit *= 1024.0*1024.0*1024.0; ss++; } + +if (limit < 0.0 || *ss != '\0') + return ratelimit_error(log_msgptr, + "\"%s\" is not a positive number", sender_rate_limit); + +/* Second is the rate measurement period / exponential smoothing time +constant. This must be strictly greater than zero, because zero leads to +run-time division errors. */ + +period = !(sender_rate_period = string_nextinlist(&arg, &sep, NULL, 0)) + ? -1.0 : readconf_readtime(sender_rate_period, 0, FALSE); +if (period <= 0.0) + return ratelimit_error(log_msgptr, + "\"%s\" is not a time value", sender_rate_period); + +/* By default we are counting one of something, but the per_rcpt, +per_byte, and count options can change this. */ + +count = 1.0; + +/* Parse the other options. */ + +while ((ss = string_nextinlist(&arg, &sep, NULL, 0))) + { + if (strcmpic(ss, US"leaky") == 0) leaky = TRUE; + else if (strcmpic(ss, US"strict") == 0) strict = TRUE; + else if (strcmpic(ss, US"noupdate") == 0) noupdate = TRUE; + else if (strcmpic(ss, US"readonly") == 0) readonly = TRUE; + else if (strcmpic(ss, US"per_cmd") == 0) RATE_SET(mode, PER_CMD); + else if (strcmpic(ss, US"per_conn") == 0) + { + RATE_SET(mode, PER_CONN); + if (where == ACL_WHERE_NOTSMTP || where == ACL_WHERE_NOTSMTP_START) + badacl = TRUE; + } + else if (strcmpic(ss, US"per_mail") == 0) + { + RATE_SET(mode, PER_MAIL); + if (where > ACL_WHERE_NOTSMTP) badacl = TRUE; + } + else if (strcmpic(ss, US"per_rcpt") == 0) + { + /* If we are running in the RCPT ACL, then we'll count the recipients + one by one, but if we are running when we have accumulated the whole + list then we'll add them all in one batch. */ + if (where == ACL_WHERE_RCPT) + RATE_SET(mode, PER_RCPT); + else if (where >= ACL_WHERE_PREDATA && where <= ACL_WHERE_NOTSMTP) + RATE_SET(mode, PER_ALLRCPTS), count = (double)recipients_count; + else if (where == ACL_WHERE_MAIL || where > ACL_WHERE_NOTSMTP) + RATE_SET(mode, PER_RCPT), badacl = TRUE; + } + else if (strcmpic(ss, US"per_byte") == 0) + { + /* If we have not yet received the message data and there was no SIZE + declaration on the MAIL command, then it's safe to just use a value of + zero and let the recorded rate decay as if nothing happened. */ + RATE_SET(mode, PER_MAIL); + if (where > ACL_WHERE_NOTSMTP) badacl = TRUE; + else count = message_size < 0 ? 0.0 : (double)message_size; + } + else if (strcmpic(ss, US"per_addr") == 0) + { + RATE_SET(mode, PER_RCPT); + if (where != ACL_WHERE_RCPT) badacl = TRUE, unique = US"*"; + else unique = string_sprintf("%s@%s", deliver_localpart, deliver_domain); + } + else if (strncmpic(ss, US"count=", 6) == 0) + { + uschar *e; + count = Ustrtod(ss+6, &e); + if (count < 0.0 || *e != '\0') + return ratelimit_error(log_msgptr, "\"%s\" is not a positive number", ss); + } + else if (strncmpic(ss, US"unique=", 7) == 0) + unique = string_copy(ss + 7); + else if (!key) + key = string_copy(ss); + else + key = string_sprintf("%s/%s", key, ss); + } + +/* Sanity check. When the badacl flag is set the update mode must either +be readonly (which is the default if it is omitted) or, for backwards +compatibility, a combination of noupdate and strict or leaky. */ + +if (mode == RATE_PER_CLASH) + return ratelimit_error(log_msgptr, "conflicting per_* options"); +if (leaky + strict + readonly > 1) + return ratelimit_error(log_msgptr, "conflicting update modes"); +if (badacl && (leaky || strict) && !noupdate) + return ratelimit_error(log_msgptr, + "\"%s\" must not have /leaky or /strict option, or cannot be used in %s ACL", + ratelimit_option_string[mode], acl_wherenames[where]); + +/* Set the default values of any unset options. In readonly mode we +perform the rate computation without any increment so that its value +decays to eventually allow over-limit senders through. */ + +if (noupdate) readonly = TRUE, leaky = strict = FALSE; +if (badacl) readonly = TRUE; +if (readonly) count = 0.0; +if (!strict && !readonly) leaky = TRUE; +if (mode == RATE_PER_WHAT) mode = RATE_PER_MAIL; + +/* Create the lookup key. If there is no explicit key, use sender_host_address. +If there is no sender_host_address (e.g. -bs or acl_not_smtp) then we simply +omit it. The smoothing constant (sender_rate_period) and the per_xxx options +are added to the key because they alter the meaning of the stored data. */ + +if (!key) + key = !sender_host_address ? US"" : sender_host_address; + +key = string_sprintf("%s/%s/%s%s", + sender_rate_period, + ratelimit_option_string[mode], + unique == NULL ? "" : "unique/", + key); + +HDEBUG(D_acl) + debug_printf_indent("ratelimit condition count=%.0f %.1f/%s\n", count, limit, key); + +/* See if we have already computed the rate by looking in the relevant tree. +For per-connection rate limiting, store tree nodes and dbdata in the permanent +pool so that they survive across resets. In readonly mode we only remember the +result for the rest of this command in case a later command changes it. After +this bit of logic the code is independent of the per_* mode. */ + +old_pool = store_pool; + +if (readonly) + anchor = &ratelimiters_cmd; +else switch(mode) + { + case RATE_PER_CONN: + anchor = &ratelimiters_conn; + store_pool = POOL_PERM; + break; + case RATE_PER_BYTE: + case RATE_PER_MAIL: + case RATE_PER_ALLRCPTS: + anchor = &ratelimiters_mail; + break; + case RATE_PER_ADDR: + case RATE_PER_CMD: + case RATE_PER_RCPT: + anchor = &ratelimiters_cmd; + break; + default: + anchor = NULL; /* silence an "unused" complaint */ + log_write(0, LOG_MAIN|LOG_PANIC_DIE, + "internal ACL error: unknown ratelimit mode %d", mode); + break; + } + +if ((t = tree_search(*anchor, key))) + { + dbd = t->data.ptr; + /* The following few lines duplicate some of the code below. */ + rc = (dbd->rate < limit)? FAIL : OK; + store_pool = old_pool; + sender_rate = string_sprintf("%.1f", dbd->rate); + HDEBUG(D_acl) + debug_printf_indent("ratelimit found pre-computed rate %s\n", sender_rate); + return rc; + } + +/* We aren't using a pre-computed rate, so get a previously recorded rate +from the database, which will be updated and written back if required. */ + +if (!(dbm = dbfn_open(US"ratelimit", O_RDWR, &dbblock, TRUE, TRUE))) + { + store_pool = old_pool; + sender_rate = NULL; + HDEBUG(D_acl) debug_printf_indent("ratelimit database not available\n"); + *log_msgptr = US"ratelimit database not available"; + return DEFER; + } +dbdb = dbfn_read_with_length(dbm, key, &dbdb_size); +dbd = NULL; + +gettimeofday(&tv, NULL); + +if (dbdb) + { + /* Locate the basic ratelimit block inside the DB data. */ + HDEBUG(D_acl) debug_printf_indent("ratelimit found key in database\n"); + dbd = &dbdb->dbd; + + /* Forget the old Bloom filter if it is too old, so that we count each + repeating event once per period. We don't simply clear and re-use the old + filter because we want its size to change if the limit changes. Note that + we keep the dbd pointer for copying the rate into the new data block. */ + + if(unique && tv.tv_sec > dbdb->bloom_epoch + period) + { + HDEBUG(D_acl) debug_printf_indent("ratelimit discarding old Bloom filter\n"); + dbdb = NULL; + } + + /* Sanity check. */ + + if(unique && dbdb_size < sizeof(*dbdb)) + { + HDEBUG(D_acl) debug_printf_indent("ratelimit discarding undersize Bloom filter\n"); + dbdb = NULL; + } + } + +/* Allocate a new data block if the database lookup failed +or the Bloom filter passed its age limit. */ + +if (!dbdb) + { + if (!unique) + { + /* No Bloom filter. This basic ratelimit block is initialized below. */ + HDEBUG(D_acl) debug_printf_indent("ratelimit creating new rate data block\n"); + dbdb_size = sizeof(*dbd); + dbdb = store_get(dbdb_size, GET_UNTAINTED); + } + else + { + int extra; + HDEBUG(D_acl) debug_printf_indent("ratelimit creating new Bloom filter\n"); + + /* See the long comment below for an explanation of the magic number 2. + The filter has a minimum size in case the rate limit is very small; + this is determined by the definition of dbdata_ratelimit_unique. */ + + extra = (int)limit * 2 - sizeof(dbdb->bloom); + if (extra < 0) extra = 0; + dbdb_size = sizeof(*dbdb) + extra; + dbdb = store_get(dbdb_size, GET_UNTAINTED); + dbdb->bloom_epoch = tv.tv_sec; + dbdb->bloom_size = sizeof(dbdb->bloom) + extra; + memset(dbdb->bloom, 0, dbdb->bloom_size); + + /* Preserve any basic ratelimit data (which is our longer-term memory) + by copying it from the discarded block. */ + + if (dbd) + { + dbdb->dbd = *dbd; + dbd = &dbdb->dbd; + } + } + } + +/* If we are counting unique events, find out if this event is new or not. +If the client repeats the event during the current period then it should be +counted. We skip this code in readonly mode for efficiency, because any +changes to the filter will be discarded and because count is already set to +zero. */ + +if (unique && !readonly) + { + /* We identify unique events using a Bloom filter. (You can find my + notes on Bloom filters at http://fanf.livejournal.com/81696.html) + With the per_addr option, an "event" is a recipient address, though the + user can use the unique option to define their own events. We only count + an event if we have not seen it before. + + We size the filter according to the rate limit, which (in leaky mode) + is the limit on the population of the filter. We allow 16 bits of space + per entry (see the construction code above) and we set (up to) 8 of them + when inserting an element (see the loop below). The probability of a false + positive (an event we have not seen before but which we fail to count) is + + size = limit * 16 + numhash = 8 + allzero = exp(-numhash * pop / size) + = exp(-0.5 * pop / limit) + fpr = pow(1 - allzero, numhash) + + For senders at the limit the fpr is 0.06% or 1 in 1700 + and for senders at half the limit it is 0.0006% or 1 in 170000 + + In strict mode the Bloom filter can fill up beyond the normal limit, in + which case the false positive rate will rise. This means that the + measured rate for very fast senders can bogusly drop off after a while. + + At twice the limit, the fpr is 2.5% or 1 in 40 + At four times the limit, it is 31% or 1 in 3.2 + + It takes ln(pop/limit) periods for an over-limit burst of pop events to + decay below the limit, and if this is more than one then the Bloom filter + will be discarded before the decay gets that far. The false positive rate + at this threshold is 9.3% or 1 in 10.7. */ + + BOOL seen; + unsigned n, hash, hinc; + uschar md5sum[16]; + md5 md5info; + + /* Instead of using eight independent hash values, we combine two values + using the formula h1 + n * h2. This does not harm the Bloom filter's + performance, and means the amount of hash we need is independent of the + number of bits we set in the filter. */ + + md5_start(&md5info); + md5_end(&md5info, unique, Ustrlen(unique), md5sum); + hash = md5sum[0] | md5sum[1] << 8 | md5sum[2] << 16 | md5sum[3] << 24; + hinc = md5sum[4] | md5sum[5] << 8 | md5sum[6] << 16 | md5sum[7] << 24; + + /* Scan the bits corresponding to this event. A zero bit means we have + not seen it before. Ensure all bits are set to record this event. */ + + HDEBUG(D_acl) debug_printf_indent("ratelimit checking uniqueness of %s\n", unique); + + seen = TRUE; + for (n = 0; n < 8; n++, hash += hinc) + { + int bit = 1 << (hash % 8); + int byte = (hash / 8) % dbdb->bloom_size; + if ((dbdb->bloom[byte] & bit) == 0) + { + dbdb->bloom[byte] |= bit; + seen = FALSE; + } + } + + /* If this event has occurred before, do not count it. */ + + if (seen) + { + HDEBUG(D_acl) debug_printf_indent("ratelimit event found in Bloom filter\n"); + count = 0.0; + } + else + HDEBUG(D_acl) debug_printf_indent("ratelimit event added to Bloom filter\n"); + } + +/* If there was no previous ratelimit data block for this key, initialize +the new one, otherwise update the block from the database. The initial rate +is what would be computed by the code below for an infinite interval. */ + +if (!dbd) + { + HDEBUG(D_acl) debug_printf_indent("ratelimit initializing new key's rate data\n"); + dbd = &dbdb->dbd; + dbd->time_stamp = tv.tv_sec; + dbd->time_usec = tv.tv_usec; + dbd->rate = count; + } +else + { + /* The smoothed rate is computed using an exponentially weighted moving + average adjusted for variable sampling intervals. The standard EWMA for + a fixed sampling interval is: f'(t) = (1 - a) * f(t) + a * f'(t - 1) + where f() is the measured value and f'() is the smoothed value. + + Old data decays out of the smoothed value exponentially, such that data n + samples old is multiplied by a^n. The exponential decay time constant p + is defined such that data p samples old is multiplied by 1/e, which means + that a = exp(-1/p). We can maintain the same time constant for a variable + sampling interval i by using a = exp(-i/p). + + The rate we are measuring is messages per period, suitable for directly + comparing with the limit. The average rate between now and the previous + message is period / interval, which we feed into the EWMA as the sample. + + It turns out that the number of messages required for the smoothed rate + to reach the limit when they are sent in a burst is equal to the limit. + This can be seen by analysing the value of the smoothed rate after N + messages sent at even intervals. Let k = (1 - a) * p/i + + rate_1 = (1 - a) * p/i + a * rate_0 + = k + a * rate_0 + rate_2 = k + a * rate_1 + = k + a * k + a^2 * rate_0 + rate_3 = k + a * k + a^2 * k + a^3 * rate_0 + rate_N = rate_0 * a^N + k * SUM(x=0..N-1)(a^x) + = rate_0 * a^N + k * (1 - a^N) / (1 - a) + = rate_0 * a^N + p/i * (1 - a^N) + + When N is large, a^N -> 0 so rate_N -> p/i as desired. + + rate_N = p/i + (rate_0 - p/i) * a^N + a^N = (rate_N - p/i) / (rate_0 - p/i) + N * -i/p = log((rate_N - p/i) / (rate_0 - p/i)) + N = p/i * log((rate_0 - p/i) / (rate_N - p/i)) + + Numerical analysis of the above equation, setting the computed rate to + increase from rate_0 = 0 to rate_N = limit, shows that for large sending + rates, p/i, the number of messages N = limit. So limit serves as both the + maximum rate measured in messages per period, and the maximum number of + messages that can be sent in a fast burst. */ + + double this_time = (double)tv.tv_sec + + (double)tv.tv_usec / 1000000.0; + double prev_time = (double)dbd->time_stamp + + (double)dbd->time_usec / 1000000.0; + + /* We must avoid division by zero, and deal gracefully with the clock going + backwards. If we blunder ahead when time is in reverse then the computed + rate will be bogus. To be safe we clamp interval to a very small number. */ + + double interval = this_time - prev_time <= 0.0 ? 1e-9 + : this_time - prev_time; + + double i_over_p = interval / period; + double a = exp(-i_over_p); + + /* Combine the instantaneous rate (period / interval) with the previous rate + using the smoothing factor a. In order to measure sized events, multiply the + instantaneous rate by the count of bytes or recipients etc. */ + + dbd->time_stamp = tv.tv_sec; + dbd->time_usec = tv.tv_usec; + dbd->rate = (1 - a) * count / i_over_p + a * dbd->rate; + + /* When events are very widely spaced the computed rate tends towards zero. + Although this is accurate it turns out not to be useful for our purposes, + especially when the first event after a long silence is the start of a spam + run. A more useful model is that the rate for an isolated event should be the + size of the event per the period size, ignoring the lack of events outside + the current period and regardless of where the event falls in the period. So, + if the interval was so long that the calculated rate is unhelpfully small, we + re-initialize the rate. In the absence of higher-rate bursts, the condition + below is true if the interval is greater than the period. */ + + if (dbd->rate < count) dbd->rate = count; + } + +/* Clients sending at the limit are considered to be over the limit. +This matters for edge cases such as a limit of zero, when the client +should be completely blocked. */ + +rc = dbd->rate < limit ? FAIL : OK; + +/* Update the state if the rate is low or if we are being strict. If we +are in leaky mode and the sender's rate is too high, we do not update +the recorded rate in order to avoid an over-aggressive sender's retry +rate preventing them from getting any email through. If readonly is set, +neither leaky nor strict are set, so we do not do any updates. */ + +if ((rc == FAIL && leaky) || strict) + { + dbfn_write(dbm, key, dbdb, dbdb_size); + HDEBUG(D_acl) debug_printf_indent("ratelimit db updated\n"); + } +else + { + HDEBUG(D_acl) debug_printf_indent("ratelimit db not updated: %s\n", + readonly? "readonly mode" : "over the limit, but leaky"); + } + +dbfn_close(dbm); + +/* Store the result in the tree for future reference. Take the taint status +from the key for consistency even though it's unlikely we'll ever expand this. */ + +t = store_get(sizeof(tree_node) + Ustrlen(key), key); +t->data.ptr = dbd; +Ustrcpy(t->name, key); +(void)tree_insertnode(anchor, t); + +/* We create the formatted version of the sender's rate very late in +order to ensure that it is done using the correct storage pool. */ + +store_pool = old_pool; +sender_rate = string_sprintf("%.1f", dbd->rate); + +HDEBUG(D_acl) + debug_printf_indent("ratelimit computed rate %s\n", sender_rate); + +return rc; +} + + + +/************************************************* +* Handle a check for previously-seen * +*************************************************/ + +/* +ACL clauses like: seen = -5m / key=$foo / readonly + +Return is true for condition-true - but the semantics +depend heavily on the actual use-case. + +Negative times test for seen-before, positive for seen-more-recently-than +(the given interval before current time). + +All are subject to history not having been cleaned from the DB. + +Default for seen-before is to create if not present, and to +update if older than 10d (with the seen-test time). +Default for seen-since is to always create or update. + +Options: + key=value. Default key is $sender_host_address + readonly + write + refresh=: update an existing DB entry older than given + amount. Default refresh lacking this option is 10d. + The update sets the record timestamp to the seen-test time. + +XXX do we need separate nocreate, noupdate controls? + +Arguments: + arg the option string for seen= + where ACL_WHERE_xxxx indicating which ACL this is + log_msgptr for error messages + +Returns: OK - Condition is true + FAIL - Condition is false + DEFER - Problem opening history database + ERROR - Syntax error in options +*/ + +static int +acl_seen(const uschar * arg, int where, uschar ** log_msgptr) +{ +enum { SEEN_DEFAULT, SEEN_READONLY, SEEN_WRITE }; + +const uschar * list = arg; +int slash = '/', interval, mode = SEEN_DEFAULT, yield = FAIL; +BOOL before; +int refresh = 10 * 24 * 60 * 60; /* 10 days */ +const uschar * ele, * key = sender_host_address; +open_db dbblock, * dbm; +dbdata_seen * dbd; +time_t now; + +/* Parse the first element, the time-relation. */ + +if (!(ele = string_nextinlist(&list, &slash, NULL, 0))) + goto badparse; +if ((before = *ele == '-')) + ele++; +if ((interval = readconf_readtime(ele, 0, FALSE)) < 0) + goto badparse; + +/* Remaining elements are options */ + +while ((ele = string_nextinlist(&list, &slash, NULL, 0))) + if (Ustrncmp(ele, "key=", 4) == 0) + key = ele + 4; + else if (Ustrcmp(ele, "readonly") == 0) + mode = SEEN_READONLY; + else if (Ustrcmp(ele, "write") == 0) + mode = SEEN_WRITE; + else if (Ustrncmp(ele, "refresh=", 8) == 0) + { + if ((refresh = readconf_readtime(ele + 8, 0, FALSE)) < 0) + goto badparse; + } + else + goto badopt; + +if (!(dbm = dbfn_open(US"seen", O_RDWR, &dbblock, TRUE, TRUE))) + { + HDEBUG(D_acl) debug_printf_indent("database for 'seen' not available\n"); + *log_msgptr = US"database for 'seen' not available"; + return DEFER; + } + +dbd = dbfn_read_with_length(dbm, key, NULL); +now = time(NULL); +if (dbd) /* an existing record */ + { + time_t diff = now - dbd->time_stamp; /* time since the record was written */ + + if (before ? diff >= interval : diff < interval) + yield = OK; + + if (mode == SEEN_READONLY) + { HDEBUG(D_acl) debug_printf_indent("seen db not written (readonly)\n"); } + else if (mode == SEEN_WRITE || !before) + { + dbd->time_stamp = now; + dbfn_write(dbm, key, dbd, sizeof(*dbd)); + HDEBUG(D_acl) debug_printf_indent("seen db written (update)\n"); + } + else if (diff >= refresh) + { + dbd->time_stamp = now - interval; + dbfn_write(dbm, key, dbd, sizeof(*dbd)); + HDEBUG(D_acl) debug_printf_indent("seen db written (refresh)\n"); + } + } +else + { /* No record found, yield always FAIL */ + if (mode != SEEN_READONLY) + { + dbdata_seen d = {.time_stamp = now}; + dbfn_write(dbm, key, &d, sizeof(*dbd)); + HDEBUG(D_acl) debug_printf_indent("seen db written (create)\n"); + } + else + HDEBUG(D_acl) debug_printf_indent("seen db not written (readonly)\n"); + } + +dbfn_close(dbm); +return yield; + + +badparse: + *log_msgptr = string_sprintf("failed to parse '%s'", arg); + return ERROR; +badopt: + *log_msgptr = string_sprintf("unrecognised option '%s' in '%s'", ele, arg); + return ERROR; +} + + + +/************************************************* +* The udpsend ACL modifier * +*************************************************/ + +/* Called by acl_check_condition() below. + +Arguments: + arg the option string for udpsend= + log_msgptr for error messages + +Returns: OK - Completed. + DEFER - Problem with DNS lookup. + ERROR - Syntax error in options. +*/ + +static int +acl_udpsend(const uschar *arg, uschar **log_msgptr) +{ +int sep = 0; +uschar *hostname; +uschar *portstr; +uschar *portend; +host_item *h; +int portnum; +int len; +int r, s; +uschar * errstr; + +hostname = string_nextinlist(&arg, &sep, NULL, 0); +portstr = string_nextinlist(&arg, &sep, NULL, 0); + +if (!hostname) + { + *log_msgptr = US"missing destination host in \"udpsend\" modifier"; + return ERROR; + } +if (!portstr) + { + *log_msgptr = US"missing destination port in \"udpsend\" modifier"; + return ERROR; + } +if (!arg) + { + *log_msgptr = US"missing datagram payload in \"udpsend\" modifier"; + return ERROR; + } +portnum = Ustrtol(portstr, &portend, 10); +if (*portend != '\0') + { + *log_msgptr = US"bad destination port in \"udpsend\" modifier"; + return ERROR; + } + +/* Make a single-item host list. */ +h = store_get(sizeof(host_item), GET_UNTAINTED); +memset(h, 0, sizeof(host_item)); +h->name = hostname; +h->port = portnum; +h->mx = MX_NONE; + +if (string_is_ip_address(hostname, NULL)) + h->address = hostname, r = HOST_FOUND; +else + r = host_find_byname(h, NULL, 0, NULL, FALSE); +if (r == HOST_FIND_FAILED || r == HOST_FIND_AGAIN) + { + *log_msgptr = US"DNS lookup failed in \"udpsend\" modifier"; + return DEFER; + } + +HDEBUG(D_acl) + debug_printf_indent("udpsend [%s]:%d %s\n", h->address, portnum, arg); + +/*XXX this could better use sendto */ +r = s = ip_connectedsocket(SOCK_DGRAM, h->address, portnum, portnum, + 1, NULL, &errstr, NULL); +if (r < 0) goto defer; +len = Ustrlen(arg); +r = send(s, arg, len, 0); +if (r < 0) + { + errstr = US strerror(errno); + close(s); + goto defer; + } +close(s); +if (r < len) + { + *log_msgptr = + string_sprintf("\"udpsend\" truncated from %d to %d octets", len, r); + return DEFER; + } + +HDEBUG(D_acl) + debug_printf_indent("udpsend %d bytes\n", r); + +return OK; + +defer: +*log_msgptr = string_sprintf("\"udpsend\" failed: %s", errstr); +return DEFER; +} + + + +/************************************************* +* Handle conditions/modifiers on an ACL item * +*************************************************/ + +/* Called from acl_check() below. + +Arguments: + verb ACL verb + cb ACL condition block - if NULL, result is OK + where where called from + addr the address being checked for RCPT, or NULL + level the nesting level + epp pointer to pass back TRUE if "endpass" encountered + (applies only to "accept" and "discard") + user_msgptr user message pointer + log_msgptr log message pointer + basic_errno pointer to where to put verify error + +Returns: OK - all conditions are met + DISCARD - an "acl" condition returned DISCARD - only allowed + for "accept" or "discard" verbs + FAIL - at least one condition fails + FAIL_DROP - an "acl" condition returned FAIL_DROP + DEFER - can't tell at the moment (typically, lookup defer, + but can be temporary callout problem) + ERROR - ERROR from nested ACL or expansion failure or other + error +*/ + +static int +acl_check_condition(int verb, acl_condition_block *cb, int where, + address_item *addr, int level, BOOL *epp, uschar **user_msgptr, + uschar **log_msgptr, int *basic_errno) +{ +uschar *user_message = NULL; +uschar *log_message = NULL; +int rc = OK; +#ifdef WITH_CONTENT_SCAN +int sep = -'/'; +#endif + +for (; cb; cb = cb->next) + { + const uschar *arg; + int control_type; + + /* The message and log_message items set up messages to be used in + case of rejection. They are expanded later. */ + + if (cb->type == ACLC_MESSAGE) + { + HDEBUG(D_acl) debug_printf_indent(" message: %s\n", cb->arg); + user_message = cb->arg; + continue; + } + + if (cb->type == ACLC_LOG_MESSAGE) + { + HDEBUG(D_acl) debug_printf_indent("l_message: %s\n", cb->arg); + log_message = cb->arg; + continue; + } + + /* The endpass "condition" just sets a flag to show it occurred. This is + checked at compile time to be on an "accept" or "discard" item. */ + + if (cb->type == ACLC_ENDPASS) + { + *epp = TRUE; + continue; + } + + /* For other conditions and modifiers, the argument is expanded now for some + of them, but not for all, because expansion happens down in some lower level + checking functions in some cases. */ + + if (!conditions[cb->type].expand_at_top) + arg = cb->arg; + else if (!(arg = expand_string(cb->arg))) + { + if (f.expand_string_forcedfail) continue; + *log_msgptr = string_sprintf("failed to expand ACL string \"%s\": %s", + cb->arg, expand_string_message); + return f.search_find_defer ? DEFER : ERROR; + } + + /* Show condition, and expanded condition if it's different */ + + HDEBUG(D_acl) + { + int lhswidth = 0; + debug_printf_indent("check %s%s %n", + (!conditions[cb->type].is_modifier && cb->u.negated)? "!":"", + conditions[cb->type].name, &lhswidth); + + if (cb->type == ACLC_SET) + { +#ifndef DISABLE_DKIM + if ( Ustrcmp(cb->u.varname, "dkim_verify_status") == 0 + || Ustrcmp(cb->u.varname, "dkim_verify_reason") == 0) + { + debug_printf("%s ", cb->u.varname); + lhswidth += 19; + } + else +#endif + { + debug_printf("acl_%s ", cb->u.varname); + lhswidth += 5 + Ustrlen(cb->u.varname); + } + } + + debug_printf("= %s\n", cb->arg); + + if (arg != cb->arg) + debug_printf("%.*s= %s\n", lhswidth, + US" ", CS arg); + } + + /* Check that this condition makes sense at this time */ + + if ((conditions[cb->type].forbids & (1 << where)) != 0) + { + *log_msgptr = string_sprintf("cannot %s %s condition in %s ACL", + conditions[cb->type].is_modifier ? "use" : "test", + conditions[cb->type].name, acl_wherenames[where]); + return ERROR; + } + + /* Run the appropriate test for each condition, or take the appropriate + action for the remaining modifiers. */ + + switch(cb->type) + { + case ACLC_ADD_HEADER: + setup_header(arg); + break; + + /* A nested ACL that returns "discard" makes sense only for an "accept" or + "discard" verb. */ + + case ACLC_ACL: + rc = acl_check_wargs(where, addr, arg, user_msgptr, log_msgptr); + if (rc == DISCARD && verb != ACL_ACCEPT && verb != ACL_DISCARD) + { + *log_msgptr = string_sprintf("nested ACL returned \"discard\" for " + "\"%s\" command (only allowed with \"accept\" or \"discard\")", + verbs[verb]); + return ERROR; + } + break; + + case ACLC_AUTHENTICATED: + rc = sender_host_authenticated ? match_isinlist(sender_host_authenticated, + &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL) : FAIL; + break; + + #ifdef EXPERIMENTAL_BRIGHTMAIL + case ACLC_BMI_OPTIN: + { + int old_pool = store_pool; + store_pool = POOL_PERM; + bmi_current_optin = string_copy(arg); + store_pool = old_pool; + } + break; + #endif + + case ACLC_CONDITION: + /* The true/false parsing here should be kept in sync with that used in + expand.c when dealing with ECOND_BOOL so that we don't have too many + different definitions of what can be a boolean. */ + if (*arg == '-' + ? Ustrspn(arg+1, "0123456789") == Ustrlen(arg+1) /* Negative number */ + : Ustrspn(arg, "0123456789") == Ustrlen(arg)) /* Digits, or empty */ + rc = (Uatoi(arg) == 0)? FAIL : OK; + else + rc = (strcmpic(arg, US"no") == 0 || + strcmpic(arg, US"false") == 0)? FAIL : + (strcmpic(arg, US"yes") == 0 || + strcmpic(arg, US"true") == 0)? OK : DEFER; + if (rc == DEFER) + *log_msgptr = string_sprintf("invalid \"condition\" value \"%s\"", arg); + break; + + case ACLC_CONTINUE: /* Always succeeds */ + break; + + case ACLC_CONTROL: + { + const uschar *p = NULL; + control_type = decode_control(arg, &p, where, log_msgptr); + + /* Check if this control makes sense at this time */ + + if (controls_list[control_type].forbids & (1 << where)) + { + *log_msgptr = string_sprintf("cannot use \"control=%s\" in %s ACL", + controls_list[control_type].name, acl_wherenames[where]); + return ERROR; + } + + switch(control_type) + { + case CONTROL_AUTH_UNADVERTISED: + f.allow_auth_unadvertised = TRUE; + break; + +#ifdef EXPERIMENTAL_BRIGHTMAIL + case CONTROL_BMI_RUN: + bmi_run = 1; + break; +#endif + +#ifndef DISABLE_DKIM + case CONTROL_DKIM_VERIFY: + f.dkim_disable_verify = TRUE; +# ifdef SUPPORT_DMARC + /* Since DKIM was blocked, skip DMARC too */ + f.dmarc_disable_verify = TRUE; + f.dmarc_enable_forensic = FALSE; +# endif + break; +#endif + +#ifdef SUPPORT_DMARC + case CONTROL_DMARC_VERIFY: + f.dmarc_disable_verify = TRUE; + break; + + case CONTROL_DMARC_FORENSIC: + f.dmarc_enable_forensic = TRUE; + break; +#endif + + case CONTROL_DSCP: + if (*p == '/') + { + int fd, af, level, optname, value; + /* If we are acting on stdin, the setsockopt may fail if stdin is not + a socket; we can accept that, we'll just debug-log failures anyway. */ + fd = fileno(smtp_in); + if ((af = ip_get_address_family(fd)) < 0) + { + HDEBUG(D_acl) + debug_printf_indent("smtp input is probably not a socket [%s], not setting DSCP\n", + strerror(errno)); + break; + } + if (dscp_lookup(p+1, af, &level, &optname, &value)) + if (setsockopt(fd, level, optname, &value, sizeof(value)) < 0) + { + HDEBUG(D_acl) debug_printf_indent("failed to set input DSCP[%s]: %s\n", + p+1, strerror(errno)); + } + else + { + HDEBUG(D_acl) debug_printf_indent("set input DSCP to \"%s\"\n", p+1); + } + else + { + *log_msgptr = string_sprintf("unrecognised DSCP value in \"control=%s\"", arg); + return ERROR; + } + } + else + { + *log_msgptr = string_sprintf("syntax error in \"control=%s\"", arg); + return ERROR; + } + break; + + case CONTROL_ERROR: + return ERROR; + + case CONTROL_CASEFUL_LOCAL_PART: + deliver_localpart = addr->cc_local_part; + break; + + case CONTROL_CASELOWER_LOCAL_PART: + deliver_localpart = addr->lc_local_part; + break; + + case CONTROL_ENFORCE_SYNC: + smtp_enforce_sync = TRUE; + break; + + case CONTROL_NO_ENFORCE_SYNC: + smtp_enforce_sync = FALSE; + break; + +#ifdef WITH_CONTENT_SCAN + case CONTROL_NO_MBOX_UNSPOOL: + f.no_mbox_unspool = TRUE; + break; +#endif + + case CONTROL_NO_MULTILINE: + f.no_multiline_responses = TRUE; + break; + + case CONTROL_NO_PIPELINING: + f.pipelining_enable = FALSE; + break; + + case CONTROL_NO_DELAY_FLUSH: + f.disable_delay_flush = TRUE; + break; + + case CONTROL_NO_CALLOUT_FLUSH: + f.disable_callout_flush = TRUE; + break; + + case CONTROL_FAKEREJECT: + cancel_cutthrough_connection(TRUE, US"fakereject"); + case CONTROL_FAKEDEFER: + fake_response = (control_type == CONTROL_FAKEDEFER) ? DEFER : FAIL; + if (*p == '/') + { + const uschar *pp = p + 1; + while (*pp) pp++; + /* The entire control= line was expanded at top so no need to expand + the part after the / */ + fake_response_text = string_copyn(p+1, pp-p-1); + p = pp; + } + else /* Explicitly reset to default string */ + fake_response_text = US"Your message has been rejected but is being kept for evaluation.\nIf it was a legitimate message, it may still be delivered to the target recipient(s)."; + break; + + case CONTROL_FREEZE: + f.deliver_freeze = TRUE; + deliver_frozen_at = time(NULL); + freeze_tell = freeze_tell_config; /* Reset to configured value */ + if (Ustrncmp(p, "/no_tell", 8) == 0) + { + p += 8; + freeze_tell = NULL; + } + if (*p) + { + *log_msgptr = string_sprintf("syntax error in \"control=%s\"", arg); + return ERROR; + } + cancel_cutthrough_connection(TRUE, US"item frozen"); + break; + + case CONTROL_QUEUE: + f.queue_only_policy = TRUE; + if (Ustrcmp(p, "_only") == 0) + p += 5; + else while (*p == '/') + if (Ustrncmp(p, "/only", 5) == 0) + { p += 5; f.queue_smtp = FALSE; } + else if (Ustrncmp(p, "/first_pass_route", 17) == 0) + { p += 17; f.queue_smtp = TRUE; } + else + break; + cancel_cutthrough_connection(TRUE, US"queueing forced"); + break; + + case CONTROL_SUBMISSION: + originator_name = US""; + f.submission_mode = TRUE; + while (*p == '/') + { + if (Ustrncmp(p, "/sender_retain", 14) == 0) + { + p += 14; + f.active_local_sender_retain = TRUE; + f.active_local_from_check = FALSE; + } + else if (Ustrncmp(p, "/domain=", 8) == 0) + { + const uschar *pp = p + 8; + while (*pp && *pp != '/') pp++; + submission_domain = string_copyn(p+8, pp-p-8); + p = pp; + } + /* The name= option must be last, because it swallows the rest of + the string. */ + else if (Ustrncmp(p, "/name=", 6) == 0) + { + const uschar *pp = p + 6; + while (*pp) pp++; + submission_name = parse_fix_phrase(p+6, pp-p-6); + p = pp; + } + else break; + } + if (*p) + { + *log_msgptr = string_sprintf("syntax error in \"control=%s\"", arg); + return ERROR; + } + break; + + case CONTROL_DEBUG: + { + uschar * debug_tag = NULL, * debug_opts = NULL; + BOOL kill = FALSE, stop = FALSE; + + while (*p == '/') + { + const uschar * pp = p+1; + if (Ustrncmp(pp, "tag=", 4) == 0) + { + for (pp += 4; *pp && *pp != '/';) pp++; + debug_tag = string_copyn(p+5, pp-p-5); + } + else if (Ustrncmp(pp, "opts=", 5) == 0) + { + for (pp += 5; *pp && *pp != '/';) pp++; + debug_opts = string_copyn(p+6, pp-p-6); + } + else if (Ustrncmp(pp, "kill", 4) == 0) + { + pp += 4; + kill = TRUE; + } + else if (Ustrncmp(pp, "stop", 4) == 0) + { + pp += 4; + stop = TRUE; + } + else if (Ustrncmp(pp, "pretrigger=", 11) == 0) + debug_pretrigger_setup(pp+11); + else if (Ustrncmp(pp, "trigger=", 8) == 0) + { + if (Ustrncmp(pp += 8, "now", 3) == 0) + { + pp += 3; + debug_trigger_fire(); + } + else if (Ustrncmp(pp, "paniclog", 8) == 0) + { + pp += 8; + dtrigger_selector |= BIT(DTi_panictrigger); + } + } + while (*pp && *pp != '/') pp++; + p = pp; + } + + if (kill) + debug_logging_stop(TRUE); + else if (stop) + debug_logging_stop(FALSE); + else if (debug_tag || debug_opts) + debug_logging_activate(debug_tag, debug_opts); + break; + } + + case CONTROL_SUPPRESS_LOCAL_FIXUPS: + f.suppress_local_fixups = TRUE; + break; + + case CONTROL_CUTTHROUGH_DELIVERY: + { + uschar * ignored = NULL; +#ifndef DISABLE_PRDR + if (prdr_requested) +#else + if (0) +#endif + /* Too hard to think about for now. We might in future cutthrough + the case where both sides handle prdr and this-node prdr acl + is "accept" */ + ignored = US"PRDR active"; + else if (f.deliver_freeze) + ignored = US"frozen"; + else if (f.queue_only_policy) + ignored = US"queue-only"; + else if (fake_response == FAIL) + ignored = US"fakereject"; + else if (rcpt_count != 1) + ignored = US"nonfirst rcpt"; + else if (cutthrough.delivery) + ignored = US"repeated"; + else if (cutthrough.callout_hold_only) + { + DEBUG(D_acl) + debug_printf_indent(" cutthrough request upgrades callout hold\n"); + cutthrough.callout_hold_only = FALSE; + cutthrough.delivery = TRUE; /* control accepted */ + } + else + { + cutthrough.delivery = TRUE; /* control accepted */ + while (*p == '/') + { + const uschar * pp = p+1; + if (Ustrncmp(pp, "defer=", 6) == 0) + { + pp += 6; + if (Ustrncmp(pp, "pass", 4) == 0) cutthrough.defer_pass = TRUE; + /* else if (Ustrncmp(pp, "spool") == 0) ; default */ + } + else + while (*pp && *pp != '/') pp++; + p = pp; + } + } + + DEBUG(D_acl) if (ignored) + debug_printf(" cutthrough request ignored on %s item\n", ignored); + } + break; + +#ifdef SUPPORT_I18N + case CONTROL_UTF8_DOWNCONVERT: + if (*p == '/') + { + if (p[1] == '1') + { + message_utf8_downconvert = 1; + addr->prop.utf8_downcvt = TRUE; + addr->prop.utf8_downcvt_maybe = FALSE; + p += 2; + break; + } + if (p[1] == '0') + { + message_utf8_downconvert = 0; + addr->prop.utf8_downcvt = FALSE; + addr->prop.utf8_downcvt_maybe = FALSE; + p += 2; + break; + } + if (p[1] == '-' && p[2] == '1') + { + message_utf8_downconvert = -1; + addr->prop.utf8_downcvt = FALSE; + addr->prop.utf8_downcvt_maybe = TRUE; + p += 3; + break; + } + *log_msgptr = US"bad option value for control=utf8_downconvert"; + } + else + { + message_utf8_downconvert = 1; + addr->prop.utf8_downcvt = TRUE; + addr->prop.utf8_downcvt_maybe = FALSE; + break; + } + return ERROR; +#endif + + } + break; + } + + #ifdef EXPERIMENTAL_DCC + case ACLC_DCC: + { + /* Separate the regular expression and any optional parameters. */ + const uschar * list = arg; + uschar *ss = string_nextinlist(&list, &sep, NULL, 0); + /* Run the dcc backend. */ + rc = dcc_process(&ss); + /* Modify return code based upon the existence of options. */ + while ((ss = string_nextinlist(&list, &sep, NULL, 0))) + if (strcmpic(ss, US"defer_ok") == 0 && rc == DEFER) + rc = FAIL; /* FAIL so that the message is passed to the next ACL */ + } + break; + #endif + + #ifdef WITH_CONTENT_SCAN + case ACLC_DECODE: + rc = mime_decode(&arg); + break; + #endif + + case ACLC_DELAY: + { + int delay = readconf_readtime(arg, 0, FALSE); + if (delay < 0) + { + *log_msgptr = string_sprintf("syntax error in argument for \"delay\" " + "modifier: \"%s\" is not a time value", arg); + return ERROR; + } + else + { + HDEBUG(D_acl) debug_printf_indent("delay modifier requests %d-second delay\n", + delay); + if (host_checking) + { + HDEBUG(D_acl) + debug_printf_indent("delay skipped in -bh checking mode\n"); + } + + /* NOTE 1: Remember that we may be + dealing with stdin/stdout here, in addition to TCP/IP connections. + Also, delays may be specified for non-SMTP input, where smtp_out and + smtp_in will be NULL. Whatever is done must work in all cases. + + NOTE 2: The added feature of flushing the output before a delay must + apply only to SMTP input. Hence the test for smtp_out being non-NULL. + */ + + else + { + if (smtp_out && !f.disable_delay_flush) + mac_smtp_fflush(); + +#if !defined(NO_POLL_H) && defined (POLLRDHUP) + { + struct pollfd p; + nfds_t n = 0; + if (smtp_out) + { + p.fd = fileno(smtp_out); + p.events = POLLRDHUP; + n = 1; + } + if (poll(&p, n, delay*1000) > 0) + HDEBUG(D_acl) debug_printf_indent("delay cancelled by peer close\n"); + } +#else + /* Lacking POLLRDHUP it appears to be impossible to detect that a + TCP/IP connection has gone away without reading from it. This means + that we cannot shorten the delay below if the client goes away, + because we cannot discover that the client has closed its end of the + connection. (The connection is actually in a half-closed state, + waiting for the server to close its end.) It would be nice to be able + to detect this state, so that the Exim process is not held up + unnecessarily. However, it seems that we can't. The poll() function + does not do the right thing, and in any case it is not always + available. */ + + while (delay > 0) delay = sleep(delay); +#endif + } + } + } + break; + +#ifndef DISABLE_DKIM + case ACLC_DKIM_SIGNER: + if (dkim_cur_signer) + rc = match_isinlist(dkim_cur_signer, + &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL); + else + rc = FAIL; + break; + + case ACLC_DKIM_STATUS: + rc = match_isinlist(dkim_verify_status, + &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL); + break; +#endif + +#ifdef SUPPORT_DMARC + case ACLC_DMARC_STATUS: + if (!f.dmarc_has_been_checked) + dmarc_process(); + f.dmarc_has_been_checked = TRUE; + /* used long way of dmarc_exim_expand_query() in case we need more + * view into the process in the future. */ + rc = match_isinlist(dmarc_exim_expand_query(DMARC_VERIFY_STATUS), + &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL); + break; +#endif + + case ACLC_DNSLISTS: + rc = verify_check_dnsbl(where, &arg, log_msgptr); + break; + + case ACLC_DOMAINS: + rc = match_isinlist(addr->domain, &arg, 0, &domainlist_anchor, + addr->domain_cache, MCL_DOMAIN, TRUE, CUSS &deliver_domain_data); + break; + + /* The value in tls_cipher is the full cipher name, for example, + TLSv1:DES-CBC3-SHA:168, whereas the values to test for are just the + cipher names such as DES-CBC3-SHA. But program defensively. We don't know + what may in practice come out of the SSL library - which at the time of + writing is poorly documented. */ + + case ACLC_ENCRYPTED: + if (tls_in.cipher == NULL) rc = FAIL; else + { + uschar *endcipher = NULL; + uschar *cipher = Ustrchr(tls_in.cipher, ':'); + if (!cipher) cipher = tls_in.cipher; else + { + endcipher = Ustrchr(++cipher, ':'); + if (endcipher) *endcipher = 0; + } + rc = match_isinlist(cipher, &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL); + if (endcipher) *endcipher = ':'; + } + break; + + /* Use verify_check_this_host() instead of verify_check_host() so that + we can pass over &host_data to catch any looked up data. Once it has been + set, it retains its value so that it's still there if another ACL verb + comes through here and uses the cache. However, we must put it into + permanent store in case it is also expected to be used in a subsequent + message in the same SMTP connection. */ + + case ACLC_HOSTS: + rc = verify_check_this_host(&arg, sender_host_cache, NULL, + sender_host_address ? sender_host_address : US"", CUSS &host_data); + if (rc == DEFER) *log_msgptr = search_error_message; + if (host_data) host_data = string_copy_perm(host_data, TRUE); + break; + + case ACLC_LOCAL_PARTS: + rc = match_isinlist(addr->cc_local_part, &arg, 0, + &localpartlist_anchor, addr->localpart_cache, MCL_LOCALPART, TRUE, + CUSS &deliver_localpart_data); + break; + + case ACLC_LOG_REJECT_TARGET: + { + int logbits = 0; + int sep = 0; + const uschar *s = arg; + uschar * ss; + while ((ss = string_nextinlist(&s, &sep, NULL, 0))) + { + if (Ustrcmp(ss, "main") == 0) logbits |= LOG_MAIN; + else if (Ustrcmp(ss, "panic") == 0) logbits |= LOG_PANIC; + else if (Ustrcmp(ss, "reject") == 0) logbits |= LOG_REJECT; + else + { + logbits |= LOG_MAIN|LOG_REJECT; + log_write(0, LOG_MAIN|LOG_PANIC, "unknown log name \"%s\" in " + "\"log_reject_target\" in %s ACL", ss, acl_wherenames[where]); + } + } + log_reject_target = logbits; + } + break; + + case ACLC_LOGWRITE: + { + int logbits = 0; + const uschar *s = arg; + if (*s == ':') + { + s++; + while (*s != ':') + { + if (Ustrncmp(s, "main", 4) == 0) + { logbits |= LOG_MAIN; s += 4; } + else if (Ustrncmp(s, "panic", 5) == 0) + { logbits |= LOG_PANIC; s += 5; } + else if (Ustrncmp(s, "reject", 6) == 0) + { logbits |= LOG_REJECT; s += 6; } + else + { + logbits = LOG_MAIN|LOG_PANIC; + s = string_sprintf(":unknown log name in \"%s\" in " + "\"logwrite\" in %s ACL", arg, acl_wherenames[where]); + } + if (*s == ',') s++; + } + s++; + } + while (isspace(*s)) s++; + + if (logbits == 0) logbits = LOG_MAIN; + log_write(0, logbits, "%s", string_printing(s)); + } + break; + + #ifdef WITH_CONTENT_SCAN + case ACLC_MALWARE: /* Run the malware backend. */ + { + /* Separate the regular expression and any optional parameters. */ + const uschar * list = arg; + uschar * ss = string_nextinlist(&list, &sep, NULL, 0); + uschar * opt; + BOOL defer_ok = FALSE; + int timeout = 0; + + while ((opt = string_nextinlist(&list, &sep, NULL, 0))) + if (strcmpic(opt, US"defer_ok") == 0) + defer_ok = TRUE; + else if ( strncmpic(opt, US"tmo=", 4) == 0 + && (timeout = readconf_readtime(opt+4, '\0', FALSE)) < 0 + ) + { + *log_msgptr = string_sprintf("bad timeout value in '%s'", opt); + return ERROR; + } + + rc = malware(ss, timeout); + if (rc == DEFER && defer_ok) + rc = FAIL; /* FAIL so that the message is passed to the next ACL */ + } + break; + + case ACLC_MIME_REGEX: + rc = mime_regex(&arg); + break; + #endif + + case ACLC_QUEUE: + if (is_tainted(arg)) + { + *log_msgptr = string_sprintf("Tainted name '%s' for queue not permitted", + arg); + return ERROR; + } + if (Ustrchr(arg, '/')) + { + *log_msgptr = string_sprintf( + "Directory separator not permitted in queue name: '%s'", arg); + return ERROR; + } + queue_name = string_copy_perm(arg, FALSE); + break; + + case ACLC_RATELIMIT: + rc = acl_ratelimit(arg, where, log_msgptr); + break; + + case ACLC_RECIPIENTS: + rc = match_address_list(CUS addr->address, TRUE, TRUE, &arg, NULL, -1, 0, + CUSS &recipient_data); + break; + + #ifdef WITH_CONTENT_SCAN + case ACLC_REGEX: + rc = regex(&arg); + break; + #endif + + case ACLC_REMOVE_HEADER: + setup_remove_header(arg); + break; + + case ACLC_SEEN: + rc = acl_seen(arg, where, log_msgptr); + break; + + case ACLC_SENDER_DOMAINS: + { + uschar *sdomain; + sdomain = Ustrrchr(sender_address, '@'); + sdomain = sdomain ? sdomain + 1 : US""; + rc = match_isinlist(sdomain, &arg, 0, &domainlist_anchor, + sender_domain_cache, MCL_DOMAIN, TRUE, NULL); + } + break; + + case ACLC_SENDERS: + rc = match_address_list(CUS sender_address, TRUE, TRUE, &arg, + sender_address_cache, -1, 0, CUSS &sender_data); + break; + + /* Connection variables must persist forever; message variables not */ + + case ACLC_SET: + { + int old_pool = store_pool; + if ( cb->u.varname[0] != 'm' +#ifndef DISABLE_EVENT + || event_name /* An event is being delivered */ +#endif + ) + store_pool = POOL_PERM; +#ifndef DISABLE_DKIM /* Overwriteable dkim result variables */ + if (Ustrcmp(cb->u.varname, "dkim_verify_status") == 0) + dkim_verify_status = string_copy(arg); + else if (Ustrcmp(cb->u.varname, "dkim_verify_reason") == 0) + dkim_verify_reason = string_copy(arg); + else +#endif + acl_var_create(cb->u.varname)->data.ptr = string_copy(arg); + store_pool = old_pool; + } + break; + +#ifdef WITH_CONTENT_SCAN + case ACLC_SPAM: + { + /* Separate the regular expression and any optional parameters. */ + const uschar * list = arg; + uschar *ss = string_nextinlist(&list, &sep, NULL, 0); + + rc = spam(CUSS &ss); + /* Modify return code based upon the existence of options. */ + while ((ss = string_nextinlist(&list, &sep, NULL, 0))) + if (strcmpic(ss, US"defer_ok") == 0 && rc == DEFER) + rc = FAIL; /* FAIL so that the message is passed to the next ACL */ + } + break; +#endif + +#ifdef SUPPORT_SPF + case ACLC_SPF: + rc = spf_process(&arg, sender_address, SPF_PROCESS_NORMAL); + break; + case ACLC_SPF_GUESS: + rc = spf_process(&arg, sender_address, SPF_PROCESS_GUESS); + break; +#endif + + case ACLC_UDPSEND: + rc = acl_udpsend(arg, log_msgptr); + break; + + /* If the verb is WARN, discard any user message from verification, because + such messages are SMTP responses, not header additions. The latter come + only from explicit "message" modifiers. However, put the user message into + $acl_verify_message so it can be used in subsequent conditions or modifiers + (until something changes it). */ + + case ACLC_VERIFY: + rc = acl_verify(where, addr, arg, user_msgptr, log_msgptr, basic_errno); + if (*user_msgptr) + acl_verify_message = *user_msgptr; + if (verb == ACL_WARN) *user_msgptr = NULL; + break; + + default: + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "internal ACL error: unknown " + "condition %d", cb->type); + break; + } + + /* If a condition was negated, invert OK/FAIL. */ + + if (!conditions[cb->type].is_modifier && cb->u.negated) + if (rc == OK) rc = FAIL; + else if (rc == FAIL || rc == FAIL_DROP) rc = OK; + + if (rc != OK) break; /* Conditions loop */ + } + + +/* If the result is the one for which "message" and/or "log_message" are used, +handle the values of these modifiers. If there isn't a log message set, we make +it the same as the user message. + +"message" is a user message that will be included in an SMTP response. Unless +it is empty, it overrides any previously set user message. + +"log_message" is a non-user message, and it adds to any existing non-user +message that is already set. + +Most verbs have but a single return for which the messages are relevant, but +for "discard", it's useful to have the log message both when it succeeds and +when it fails. For "accept", the message is used in the OK case if there is no +"endpass", but (for backwards compatibility) in the FAIL case if "endpass" is +present. */ + +if (*epp && rc == OK) user_message = NULL; + +if ((BIT(rc) & msgcond[verb]) != 0) + { + uschar *expmessage; + uschar *old_user_msgptr = *user_msgptr; + uschar *old_log_msgptr = (*log_msgptr != NULL)? *log_msgptr : old_user_msgptr; + + /* If the verb is "warn", messages generated by conditions (verification or + nested ACLs) are always discarded. This also happens for acceptance verbs + when they actually do accept. Only messages specified at this level are used. + However, the value of an existing message is available in $acl_verify_message + during expansions. */ + + if (verb == ACL_WARN || + (rc == OK && (verb == ACL_ACCEPT || verb == ACL_DISCARD))) + *log_msgptr = *user_msgptr = NULL; + + if (user_message) + { + acl_verify_message = old_user_msgptr; + expmessage = expand_string(user_message); + if (!expmessage) + { + if (!f.expand_string_forcedfail) + log_write(0, LOG_MAIN|LOG_PANIC, "failed to expand ACL message \"%s\": %s", + user_message, expand_string_message); + } + else if (expmessage[0] != 0) *user_msgptr = expmessage; + } + + if (log_message) + { + acl_verify_message = old_log_msgptr; + expmessage = expand_string(log_message); + if (!expmessage) + { + if (!f.expand_string_forcedfail) + log_write(0, LOG_MAIN|LOG_PANIC, "failed to expand ACL message \"%s\": %s", + log_message, expand_string_message); + } + else if (expmessage[0] != 0) + { + *log_msgptr = (*log_msgptr == NULL)? expmessage : + string_sprintf("%s: %s", expmessage, *log_msgptr); + } + } + + /* If no log message, default it to the user message */ + + if (!*log_msgptr) *log_msgptr = *user_msgptr; + } + +acl_verify_message = NULL; +return rc; +} + + + + + +/************************************************* +* Get line from a literal ACL * +*************************************************/ + +/* This function is passed to acl_read() in order to extract individual lines +of a literal ACL, which we access via static pointers. We can destroy the +contents because this is called only once (the compiled ACL is remembered). + +This code is intended to treat the data in the same way as lines in the main +Exim configuration file. That is: + + . Leading spaces are ignored. + + . A \ at the end of a line is a continuation - trailing spaces after the \ + are permitted (this is because I don't believe in making invisible things + significant). Leading spaces on the continued part of a line are ignored. + + . Physical lines starting (significantly) with # are totally ignored, and + may appear within a sequence of backslash-continued lines. + + . Blank lines are ignored, but will end a sequence of continuations. + +Arguments: none +Returns: a pointer to the next line +*/ + + +static uschar *acl_text; /* Current pointer in the text */ +static uschar *acl_text_end; /* Points one past the terminating '0' */ + + +static uschar * +acl_getline(void) +{ +uschar *yield; + +/* This loop handles leading blank lines and comments. */ + +for(;;) + { + Uskip_whitespace(&acl_text); /* Leading spaces/empty lines */ + if (!*acl_text) return NULL; /* No more data */ + yield = acl_text; /* Potential data line */ + + while (*acl_text && *acl_text != '\n') acl_text++; + + /* If we hit the end before a newline, we have the whole logical line. If + it's a comment, there's no more data to be given. Otherwise, yield it. */ + + if (!*acl_text) return *yield == '#' ? NULL : yield; + + /* After reaching a newline, end this loop if the physical line does not + start with '#'. If it does, it's a comment, and the loop continues. */ + + if (*yield != '#') break; + } + +/* This loop handles continuations. We know we have some real data, ending in +newline. See if there is a continuation marker at the end (ignoring trailing +white space). We know that *yield is not white space, so no need to test for +cont > yield in the backwards scanning loop. */ + +for(;;) + { + uschar *cont; + for (cont = acl_text - 1; isspace(*cont); cont--); + + /* If no continuation follows, we are done. Mark the end of the line and + return it. */ + + if (*cont != '\\') + { + *acl_text++ = 0; + return yield; + } + + /* We have encountered a continuation. Skip over whitespace at the start of + the next line, and indeed the whole of the next line or lines if they are + comment lines. */ + + for (;;) + { + while (*(++acl_text) == ' ' || *acl_text == '\t'); + if (*acl_text != '#') break; + while (*(++acl_text) != 0 && *acl_text != '\n'); + } + + /* We have the start of a continuation line. Move all the rest of the data + to join onto the previous line, and then find its end. If the end is not a + newline, we are done. Otherwise loop to look for another continuation. */ + + memmove(cont, acl_text, acl_text_end - acl_text); + acl_text_end -= acl_text - cont; + acl_text = cont; + while (*acl_text != 0 && *acl_text != '\n') acl_text++; + if (*acl_text == 0) return yield; + } + +/* Control does not reach here */ +} + + + + + +/************************************************/ +/* For error messages, a string describing the config location +associated with current processing. NULL if not in an ACL. */ + +uschar * +acl_current_verb(void) +{ +if (acl_current) return string_sprintf(" (ACL %s, %s %d)", + verbs[acl_current->verb], acl_current->srcfile, acl_current->srcline); +return NULL; +} + +/************************************************* +* Check access using an ACL * +*************************************************/ + +/* This function is called from address_check. It may recurse via +acl_check_condition() - hence the use of a level to stop looping. The ACL is +passed as a string which is expanded. A forced failure implies no access check +is required. If the result is a single word, it is taken as the name of an ACL +which is sought in the global ACL tree. Otherwise, it is taken as literal ACL +text, complete with newlines, and parsed as such. In both cases, the ACL check +is then run. This function uses an auxiliary function for acl_read() to call +for reading individual lines of a literal ACL. This is acl_getline(), which +appears immediately above. + +Arguments: + where where called from + addr address item when called from RCPT; otherwise NULL + s the input string; NULL is the same as an empty ACL => DENY + user_msgptr where to put a user error (for SMTP response) + log_msgptr where to put a logging message (not for SMTP response) + +Returns: OK access is granted + DISCARD access is apparently granted... + FAIL access is denied + FAIL_DROP access is denied; drop the connection + DEFER can't tell at the moment + ERROR disaster +*/ + +static int +acl_check_internal(int where, address_item *addr, uschar *s, + uschar **user_msgptr, uschar **log_msgptr) +{ +int fd = -1; +acl_block *acl = NULL; +uschar *acl_name = US"inline ACL"; +uschar *ss; + +/* Catch configuration loops */ + +if (acl_level > 20) + { + *log_msgptr = US"ACL nested too deep: possible loop"; + return ERROR; + } + +if (!s) + { + HDEBUG(D_acl) debug_printf_indent("ACL is NULL: implicit DENY\n"); + return FAIL; + } + +/* At top level, we expand the incoming string. At lower levels, it has already +been expanded as part of condition processing. */ + +if (acl_level == 0) + { + if (!(ss = expand_string(s))) + { + if (f.expand_string_forcedfail) return OK; + *log_msgptr = string_sprintf("failed to expand ACL string \"%s\": %s", s, + expand_string_message); + return ERROR; + } + } +else ss = s; + +while (isspace(*ss)) ss++; + +/* If we can't find a named ACL, the default is to parse it as an inline one. +(Unless it begins with a slash; non-existent files give rise to an error.) */ + +acl_text = ss; + +if (is_tainted(acl_text) && !f.running_in_test_harness) + { + log_write(0, LOG_MAIN|LOG_PANIC, + "attempt to use tainted ACL text \"%s\"", acl_text); + /* Avoid leaking info to an attacker */ + *log_msgptr = US"internal configuration error"; + return ERROR; + } + +/* Handle the case of a string that does not contain any spaces. Look for a +named ACL among those read from the configuration, or a previously read file. +It is possible that the pointer to the ACL is NULL if the configuration +contains a name with no data. If not found, and the text begins with '/', +read an ACL from a file, and save it so it can be re-used. */ + +if (Ustrchr(ss, ' ') == NULL) + { + tree_node * t = tree_search(acl_anchor, ss); + if (t) + { + if (!(acl = (acl_block *)(t->data.ptr))) + { + HDEBUG(D_acl) debug_printf_indent("ACL \"%s\" is empty: implicit DENY\n", ss); + return FAIL; + } + acl_name = string_sprintf("ACL \"%s\"", ss); + HDEBUG(D_acl) debug_printf_indent("using ACL \"%s\"\n", ss); + } + + else if (*ss == '/') + { + struct stat statbuf; + if ((fd = Uopen(ss, O_RDONLY, 0)) < 0) + { + *log_msgptr = string_sprintf("failed to open ACL file \"%s\": %s", ss, + strerror(errno)); + return ERROR; + } + if (fstat(fd, &statbuf) != 0) + { + *log_msgptr = string_sprintf("failed to fstat ACL file \"%s\": %s", ss, + strerror(errno)); + return ERROR; + } + + /* If the string being used as a filename is tainted, so is the file content */ + acl_text = store_get(statbuf.st_size + 1, ss); + acl_text_end = acl_text + statbuf.st_size + 1; + + if (read(fd, acl_text, statbuf.st_size) != statbuf.st_size) + { + *log_msgptr = string_sprintf("failed to read ACL file \"%s\": %s", + ss, strerror(errno)); + return ERROR; + } + acl_text[statbuf.st_size] = 0; + (void)close(fd); + + acl_name = string_sprintf("ACL \"%s\"", ss); + HDEBUG(D_acl) debug_printf_indent("read ACL from file %s\n", ss); + } + } + +/* Parse an ACL that is still in text form. If it came from a file, remember it +in the ACL tree, having read it into the POOL_PERM store pool so that it +persists between multiple messages. */ + +if (!acl) + { + int old_pool = store_pool; + if (fd >= 0) store_pool = POOL_PERM; + acl = acl_read(acl_getline, log_msgptr); + store_pool = old_pool; + if (!acl && *log_msgptr) return ERROR; + if (fd >= 0) + { + tree_node * t = store_get_perm(sizeof(tree_node) + Ustrlen(ss), ss); + Ustrcpy(t->name, ss); + t->data.ptr = acl; + (void)tree_insertnode(&acl_anchor, t); + } + } + +/* Now we have an ACL to use. It's possible it may be NULL. */ + +while ((acl_current = acl)) + { + int cond; + int basic_errno = 0; + BOOL endpass_seen = FALSE; + BOOL acl_quit_check = acl_level == 0 + && (where == ACL_WHERE_QUIT || where == ACL_WHERE_NOTQUIT); + + *log_msgptr = *user_msgptr = NULL; + f.acl_temp_details = FALSE; + + HDEBUG(D_acl) debug_printf_indent("processing \"%s\" (%s %d)\n", + verbs[acl->verb], acl->srcfile, acl->srcline); + + /* Clear out any search error message from a previous check before testing + this condition. */ + + search_error_message = NULL; + cond = acl_check_condition(acl->verb, acl->condition, where, addr, acl_level, + &endpass_seen, user_msgptr, log_msgptr, &basic_errno); + + /* Handle special returns: DEFER causes a return except on a WARN verb; + ERROR always causes a return. */ + + switch (cond) + { + case DEFER: + HDEBUG(D_acl) debug_printf_indent("%s: condition test deferred in %s\n", + verbs[acl->verb], acl_name); + if (basic_errno != ERRNO_CALLOUTDEFER) + { + if (search_error_message != NULL && *search_error_message != 0) + *log_msgptr = search_error_message; + if (smtp_return_error_details) f.acl_temp_details = TRUE; + } + else + f.acl_temp_details = TRUE; + if (acl->verb != ACL_WARN) return DEFER; + break; + + default: /* Paranoia */ + case ERROR: + HDEBUG(D_acl) debug_printf_indent("%s: condition test error in %s\n", + verbs[acl->verb], acl_name); + return ERROR; + + case OK: + HDEBUG(D_acl) debug_printf_indent("%s: condition test succeeded in %s\n", + verbs[acl->verb], acl_name); + break; + + case FAIL: + HDEBUG(D_acl) debug_printf_indent("%s: condition test failed in %s\n", + verbs[acl->verb], acl_name); + break; + + /* DISCARD and DROP can happen only from a nested ACL condition, and + DISCARD can happen only for an "accept" or "discard" verb. */ + + case DISCARD: + HDEBUG(D_acl) debug_printf_indent("%s: condition test yielded \"discard\" in %s\n", + verbs[acl->verb], acl_name); + break; + + case FAIL_DROP: + HDEBUG(D_acl) debug_printf_indent("%s: condition test yielded \"drop\" in %s\n", + verbs[acl->verb], acl_name); + break; + } + + /* At this point, cond for most verbs is either OK or FAIL or (as a result of + a nested ACL condition) FAIL_DROP. However, for WARN, cond may be DEFER, and + for ACCEPT and DISCARD, it may be DISCARD after a nested ACL call. */ + + switch(acl->verb) + { + case ACL_ACCEPT: + if (cond == OK || cond == DISCARD) + { + HDEBUG(D_acl) debug_printf_indent("end of %s: ACCEPT\n", acl_name); + return cond; + } + if (endpass_seen) + { + HDEBUG(D_acl) debug_printf_indent("accept: endpass encountered - denying access\n"); + return cond; + } + break; + + case ACL_DEFER: + if (cond == OK) + { + HDEBUG(D_acl) debug_printf_indent("end of %s: DEFER\n", acl_name); + if (acl_quit_check) goto badquit; + f.acl_temp_details = TRUE; + return DEFER; + } + break; + + case ACL_DENY: + if (cond == OK) + { + HDEBUG(D_acl) debug_printf_indent("end of %s: DENY\n", acl_name); + if (acl_quit_check) goto badquit; + return FAIL; + } + break; + + case ACL_DISCARD: + if (cond == OK || cond == DISCARD) + { + HDEBUG(D_acl) debug_printf_indent("end of %s: DISCARD\n", acl_name); + if (acl_quit_check) goto badquit; + return DISCARD; + } + if (endpass_seen) + { + HDEBUG(D_acl) + debug_printf_indent("discard: endpass encountered - denying access\n"); + return cond; + } + break; + + case ACL_DROP: + if (cond == OK) + { + HDEBUG(D_acl) debug_printf_indent("end of %s: DROP\n", acl_name); + if (acl_quit_check) goto badquit; + return FAIL_DROP; + } + break; + + case ACL_REQUIRE: + if (cond != OK) + { + HDEBUG(D_acl) debug_printf_indent("end of %s: not OK\n", acl_name); + if (acl_quit_check) goto badquit; + return cond; + } + break; + + case ACL_WARN: + if (cond == OK) + acl_warn(where, *user_msgptr, *log_msgptr); + else if (cond == DEFER && LOGGING(acl_warn_skipped)) + log_write(0, LOG_MAIN, "%s Warning: ACL \"warn\" statement skipped: " + "condition test deferred%s%s", host_and_ident(TRUE), + *log_msgptr ? US": " : US"", + *log_msgptr ? *log_msgptr : US""); + *log_msgptr = *user_msgptr = NULL; /* In case implicit DENY follows */ + break; + + default: + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "internal ACL error: unknown verb %d", + acl->verb); + break; + } + + /* Pass to the next ACL item */ + + acl = acl->next; + } + +/* We have reached the end of the ACL. This is an implicit DENY. */ + +HDEBUG(D_acl) debug_printf_indent("end of %s: implicit DENY\n", acl_name); +return FAIL; + +badquit: + *log_msgptr = string_sprintf("QUIT or not-QUIT toplevel ACL may not fail " + "('%s' verb used incorrectly)", verbs[acl->verb]); + return ERROR; +} + + + + +/* Same args as acl_check_internal() above, but the string s is +the name of an ACL followed optionally by up to 9 space-separated arguments. +The name and args are separately expanded. Args go into $acl_arg globals. */ +static int +acl_check_wargs(int where, address_item *addr, const uschar *s, + uschar **user_msgptr, uschar **log_msgptr) +{ +uschar * tmp; +uschar * tmp_arg[9]; /* must match acl_arg[] */ +uschar * sav_arg[9]; /* must match acl_arg[] */ +int sav_narg; +uschar * name; +int i; +int ret; + +if (!(tmp = string_dequote(&s)) || !(name = expand_string(tmp))) + goto bad; + +for (i = 0; i < 9; i++) + { + while (*s && isspace(*s)) s++; + if (!*s) break; + if (!(tmp = string_dequote(&s)) || !(tmp_arg[i] = expand_string(tmp))) + { + tmp = name; + goto bad; + } + } + +sav_narg = acl_narg; +acl_narg = i; +for (i = 0; i < acl_narg; i++) + { + sav_arg[i] = acl_arg[i]; + acl_arg[i] = tmp_arg[i]; + } +while (i < 9) + { + sav_arg[i] = acl_arg[i]; + acl_arg[i++] = NULL; + } + +acl_level++; +ret = acl_check_internal(where, addr, name, user_msgptr, log_msgptr); +acl_level--; + +acl_narg = sav_narg; +for (i = 0; i < 9; i++) acl_arg[i] = sav_arg[i]; +return ret; + +bad: +if (f.expand_string_forcedfail) return ERROR; +*log_msgptr = string_sprintf("failed to expand ACL string \"%s\": %s", + tmp, expand_string_message); +return f.search_find_defer ? DEFER : ERROR; +} + + + +/************************************************* +* Check access using an ACL * +*************************************************/ + +/* Alternate interface for ACL, used by expansions */ +int +acl_eval(int where, uschar *s, uschar **user_msgptr, uschar **log_msgptr) +{ +address_item adb; +address_item *addr = NULL; +int rc; + +*user_msgptr = *log_msgptr = NULL; +sender_verified_failed = NULL; +ratelimiters_cmd = NULL; +log_reject_target = LOG_MAIN|LOG_REJECT; + +if (where == ACL_WHERE_RCPT) + { + adb = address_defaults; + addr = &adb; + addr->address = expand_string(US"$local_part@$domain"); + addr->domain = deliver_domain; + addr->local_part = deliver_localpart; + addr->cc_local_part = deliver_localpart; + addr->lc_local_part = deliver_localpart; + } + +acl_level++; +rc = acl_check_internal(where, addr, s, user_msgptr, log_msgptr); +acl_level--; +return rc; +} + + + +/* This is the external interface for ACL checks. It sets up an address and the +expansions for $domain and $local_part when called after RCPT, then calls +acl_check_internal() to do the actual work. + +Arguments: + where ACL_WHERE_xxxx indicating where called from + recipient RCPT address for RCPT check, else NULL + s the input string; NULL is the same as an empty ACL => DENY + user_msgptr where to put a user error (for SMTP response) + log_msgptr where to put a logging message (not for SMTP response) + +Returns: OK access is granted by an ACCEPT verb + DISCARD access is granted by a DISCARD verb + FAIL access is denied + FAIL_DROP access is denied; drop the connection + DEFER can't tell at the moment + ERROR disaster +*/ +int acl_where = ACL_WHERE_UNKNOWN; + +int +acl_check(int where, uschar *recipient, uschar *s, uschar **user_msgptr, + uschar **log_msgptr) +{ +int rc; +address_item adb; +address_item *addr = NULL; + +*user_msgptr = *log_msgptr = NULL; +sender_verified_failed = NULL; +ratelimiters_cmd = NULL; +log_reject_target = LOG_MAIN|LOG_REJECT; + +#ifndef DISABLE_PRDR +if (where==ACL_WHERE_RCPT || where==ACL_WHERE_VRFY || where==ACL_WHERE_PRDR) +#else +if (where==ACL_WHERE_RCPT || where==ACL_WHERE_VRFY) +#endif + { + adb = address_defaults; + addr = &adb; + addr->address = recipient; + if (deliver_split_address(addr) == DEFER) + { + *log_msgptr = US"defer in percent_hack_domains check"; + return DEFER; + } +#ifdef SUPPORT_I18N + if ((addr->prop.utf8_msg = message_smtputf8)) + { + addr->prop.utf8_downcvt = message_utf8_downconvert == 1; + addr->prop.utf8_downcvt_maybe = message_utf8_downconvert == -1; + } +#endif + deliver_domain = addr->domain; + deliver_localpart = addr->local_part; + } + +acl_where = where; +acl_level = 0; +rc = acl_check_internal(where, addr, s, user_msgptr, log_msgptr); +acl_level = 0; +acl_where = ACL_WHERE_UNKNOWN; + +/* Cutthrough - if requested, +and WHERE_RCPT and not yet opened conn as result of recipient-verify, +and rcpt acl returned accept, +and first recipient (cancel on any subsequents) +open one now and run it up to RCPT acceptance. +A failed verify should cancel cutthrough request, +and will pass the fail to the originator. +Initial implementation: dual-write to spool. +Assume the rxd datastream is now being copied byte-for-byte to an open cutthrough connection. + +Cease cutthrough copy on rxd final dot; do not send one. + +On a data acl, if not accept and a cutthrough conn is open, hard-close it (no SMTP niceness). + +On data acl accept, terminate the dataphase on an open cutthrough conn. If accepted or +perm-rejected, reflect that to the original sender - and dump the spooled copy. +If temp-reject, close the conn (and keep the spooled copy). +If conn-failure, no action (and keep the spooled copy). +*/ +switch (where) + { + case ACL_WHERE_RCPT: +#ifndef DISABLE_PRDR + case ACL_WHERE_PRDR: +#endif + + if (f.host_checking_callout) /* -bhc mode */ + cancel_cutthrough_connection(TRUE, US"host-checking mode"); + + else if ( rc == OK + && cutthrough.delivery + && rcpt_count > cutthrough.nrcpt + ) + { + if ((rc = open_cutthrough_connection(addr)) == DEFER) + if (cutthrough.defer_pass) + { + uschar * s = addr->message; + /* Horrid kludge to recover target's SMTP message */ + while (*s) s++; + do --s; while (!isdigit(*s)); + if (*--s && isdigit(*s) && *--s && isdigit(*s)) *user_msgptr = s; + f.acl_temp_details = TRUE; + } + else + { + HDEBUG(D_acl) debug_printf_indent("cutthrough defer; will spool\n"); + rc = OK; + } + } + else HDEBUG(D_acl) if (cutthrough.delivery) + if (rcpt_count <= cutthrough.nrcpt) + debug_printf_indent("ignore cutthrough request; nonfirst message\n"); + else if (rc != OK) + debug_printf_indent("ignore cutthrough request; ACL did not accept\n"); + break; + + case ACL_WHERE_PREDATA: + if (rc == OK) + cutthrough_predata(); + else + cancel_cutthrough_connection(TRUE, US"predata acl not ok"); + break; + + case ACL_WHERE_QUIT: + case ACL_WHERE_NOTQUIT: + /* Drop cutthrough conns, and drop heldopen verify conns if + the previous was not DATA */ + { + uschar prev = + smtp_connection_had[SMTP_HBUFF_PREV(SMTP_HBUFF_PREV(smtp_ch_index))]; + BOOL dropverify = !(prev == SCH_DATA || prev == SCH_BDAT); + + cancel_cutthrough_connection(dropverify, US"quit or conndrop"); + break; + } + + default: + break; + } + +deliver_domain = deliver_localpart = deliver_address_data = + deliver_domain_data = sender_address_data = NULL; + +/* A DISCARD response is permitted only for message ACLs, excluding the PREDATA +ACL, which is really in the middle of an SMTP command. */ + +if (rc == DISCARD) + { + if (where > ACL_WHERE_NOTSMTP || where == ACL_WHERE_PREDATA) + { + log_write(0, LOG_MAIN|LOG_PANIC, "\"discard\" verb not allowed in %s " + "ACL", acl_wherenames[where]); + return ERROR; + } + return DISCARD; + } + +/* A DROP response is not permitted from MAILAUTH */ + +if (rc == FAIL_DROP && where == ACL_WHERE_MAILAUTH) + { + log_write(0, LOG_MAIN|LOG_PANIC, "\"drop\" verb not allowed in %s " + "ACL", acl_wherenames[where]); + return ERROR; + } + +/* Before giving a response, take a look at the length of any user message, and +split it up into multiple lines if possible. */ + +*user_msgptr = string_split_message(*user_msgptr); +if (fake_response != OK) + fake_response_text = string_split_message(fake_response_text); + +return rc; +} + + +/************************************************* +* Create ACL variable * +*************************************************/ + +/* Create an ACL variable or reuse an existing one. ACL variables are in a +binary tree (see tree.c) with acl_var_c and acl_var_m as root nodes. + +Argument: + name pointer to the variable's name, starting with c or m + +Returns the pointer to variable's tree node +*/ + +tree_node * +acl_var_create(uschar * name) +{ +tree_node * node, ** root = name[0] == 'c' ? &acl_var_c : &acl_var_m; +if (!(node = tree_search(*root, name))) + { + node = store_get(sizeof(tree_node) + Ustrlen(name), name); + Ustrcpy(node->name, name); + (void)tree_insertnode(root, node); + } +node->data.ptr = NULL; +return node; +} + + + +/************************************************* +* Write an ACL variable in spool format * +*************************************************/ + +/* This function is used as a callback for tree_walk when writing variables to +the spool file. To retain spool file compatibility, what is written is -aclc or +-aclm followed by the rest of the name and the data length, space separated, +then the value itself, starting on a new line, and terminated by an additional +newline. When we had only numbered ACL variables, the first line might look +like this: "-aclc 5 20". Now it might be "-aclc foo 20" for the variable called +acl_cfoo. + +Arguments: + name of the variable + value of the variable + ctx FILE pointer (as a void pointer) + +Returns: nothing +*/ + +void +acl_var_write(uschar * name, uschar * value, void * ctx) +{ +FILE * f = (FILE *)ctx; +putc('-', f); +if (is_tainted(value)) + { + int q = quoter_for_address(value); + putc('-', f); + if (is_real_quoter(q)) fprintf(f, "(%s)", lookup_list[q]->name); + } +fprintf(f, "acl%c %s %d\n%s\n", name[0], name+1, Ustrlen(value), value); +} + +#endif /* !MACRO_PREDEF */ +/* vi: aw ai sw=2 +*/ +/* End of acl.c */ diff --git a/src/aliases.default b/src/aliases.default new file mode 100644 index 0000000..725d172 --- /dev/null +++ b/src/aliases.default @@ -0,0 +1,40 @@ +# Default aliases file, installed by Exim. This file contains no real aliases. +# You should edit it to taste. + + +# The following alias is required by the mail RFCs 2821 and 2822. +# Set it to the address of a HUMAN who deals with this system's mail problems. + +# postmaster: someone@your.domain + +# It is also common to set the following alias so that if anybody replies to a +# bounce message from this host, the reply goes to the postmaster. + +# mailer-daemon: postmaster + + +# You should also set up an alias for messages to root, because it is not +# usually a good idea to deliver mail as root. + +# root: postmaster + +# It is a good idea to redirect any messages sent to system accounts so that +# they don't just get ignored. Here are some common examples: + +# bin: root +# daemon: root +# ftp: root +# nobody: root +# operator: root +# uucp: root + +# You should check your /etc/passwd for any others. + + +# Other commonly enountered aliases are: +# +# abuse: the person dealing with network and mail abuse +# hostmaster: the person dealing with DNS problems +# webmaster: the person dealing with your website + +#### diff --git a/src/arc.c b/src/arc.c new file mode 100644 index 0000000..86688f6 --- /dev/null +++ b/src/arc.c @@ -0,0 +1,1903 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ +/* Experimental ARC support for Exim + Copyright (c) Jeremy Harris 2018 - 2020 + Copyright (c) The Exim Maintainers 2021 - 2022 + License: GPL +*/ + +#include "exim.h" +#if defined EXPERIMENTAL_ARC +# if defined DISABLE_DKIM +# error DKIM must also be enabled for ARC +# else + +# include "functions.h" +# include "pdkim/pdkim.h" +# include "pdkim/signing.h" + +extern pdkim_ctx * dkim_verify_ctx; +extern pdkim_ctx dkim_sign_ctx; + +#define ARC_SIGN_OPT_TSTAMP BIT(0) +#define ARC_SIGN_OPT_EXPIRE BIT(1) + +#define ARC_SIGN_DEFAULT_EXPIRE_DELTA (60 * 60 * 24 * 30) /* one month */ + +/******************************************************************************/ + +typedef struct hdr_rlist { + struct hdr_rlist * prev; + BOOL used; + header_line * h; +} hdr_rlist; + +typedef struct arc_line { + header_line * complete; /* including the header name; nul-term */ + uschar * relaxed; + + /* identified tag contents */ + /*XXX t= for AS? */ + blob i; + blob cv; + blob a; + blob b; + blob bh; + blob d; + blob h; + blob s; + blob c; + blob l; + + /* tag content sub-portions */ + blob a_algo; + blob a_hash; + + blob c_head; + blob c_body; + + /* modified copy of b= field in line */ + blob rawsig_no_b_val; +} arc_line; + +typedef struct arc_set { + struct arc_set * next; + struct arc_set * prev; + + unsigned instance; + arc_line * hdr_aar; + arc_line * hdr_ams; + arc_line * hdr_as; + + const uschar * ams_verify_done; + BOOL ams_verify_passed; +} arc_set; + +typedef struct arc_ctx { + arc_set * arcset_chain; + arc_set * arcset_chain_last; +} arc_ctx; + +#define ARC_HDR_AAR US"ARC-Authentication-Results:" +#define ARC_HDRLEN_AAR 27 +#define ARC_HDR_AMS US"ARC-Message-Signature:" +#define ARC_HDRLEN_AMS 22 +#define ARC_HDR_AS US"ARC-Seal:" +#define ARC_HDRLEN_AS 9 +#define HDR_AR US"Authentication-Results:" +#define HDRLEN_AR 23 + +static time_t now; +static time_t expire; +static hdr_rlist * headers_rlist; +static arc_ctx arc_sign_ctx = { NULL }; +static arc_ctx arc_verify_ctx = { NULL }; + + +/******************************************************************************/ + + +/* Get the instance number from the header. +Return 0 on error */ +static unsigned +arc_instance_from_hdr(const arc_line * al) +{ +const uschar * s = al->i.data; +if (!s || !al->i.len) return 0; +return (unsigned) atoi(CCS s); +} + + +static uschar * +skip_fws(uschar * s) +{ +uschar c = *s; +while (c && (c == ' ' || c == '\t' || c == '\n' || c == '\r')) c = *++s; +return s; +} + + +/* Locate instance struct on chain, inserting a new one if +needed. The chain is in increasing-instance-number order +by the "next" link, and we have a "prev" link also. +*/ + +static arc_set * +arc_find_set(arc_ctx * ctx, unsigned i) +{ +arc_set ** pas, * as, * next, * prev; + +for (pas = &ctx->arcset_chain, prev = NULL, next = ctx->arcset_chain; + as = *pas; pas = &as->next) + { + if (as->instance > i) break; + if (as->instance == i) + { + DEBUG(D_acl) debug_printf("ARC: existing instance %u\n", i); + return as; + } + next = as->next; + prev = as; + } + +DEBUG(D_acl) debug_printf("ARC: new instance %u\n", i); +*pas = as = store_get(sizeof(arc_set), GET_UNTAINTED); +memset(as, 0, sizeof(arc_set)); +as->next = next; +as->prev = prev; +as->instance = i; +if (next) + next->prev = as; +else + ctx->arcset_chain_last = as; +return as; +} + + + +/* Insert a tag content into the line structure. +Note this is a reference to existing data, not a copy. +Check for already-seen tag. +The string-pointer is on the '=' for entry. Update it past the +content (to the ;) on return; +*/ + +static uschar * +arc_insert_tagvalue(arc_line * al, unsigned loff, uschar ** ss) +{ +uschar * s = *ss; +uschar c = *++s; +blob * b = (blob *)(US al + loff); +size_t len = 0; + +/* [FWS] tag-value [FWS] */ + +if (b->data) return US"fail"; +s = skip_fws(s); /* FWS */ + +b->data = s; +while ((c = *s) && c != ';') { len++; s++; } +*ss = s; +while (len && ((c = s[-1]) == ' ' || c == '\t' || c == '\n' || c == '\r')) + { s--; len--; } /* FWS */ +b->len = len; +return NULL; +} + + +/* Inspect a header line, noting known tag fields. +Check for duplicates. */ + +static uschar * +arc_parse_line(arc_line * al, header_line * h, unsigned off, BOOL instance_only) +{ +uschar * s = h->text + off; +uschar * r = NULL; /* compiler-quietening */ +uschar c; + +al->complete = h; + +if (!instance_only) + { + al->rawsig_no_b_val.data = store_get(h->slen + 1, GET_TAINTED); + memcpy(al->rawsig_no_b_val.data, h->text, off); /* copy the header name blind */ + r = al->rawsig_no_b_val.data + off; + al->rawsig_no_b_val.len = off; + } + +/* tag-list = tag-spec *( ";" tag-spec ) [ ";" ] */ + +while ((c = *s)) + { + char tagchar; + uschar * t; + unsigned i = 0; + uschar * fieldstart = s; + uschar * bstart = NULL, * bend; + + /* tag-spec = [FWS] tag-name [FWS] "=" [FWS] tag-value [FWS] */ + + s = skip_fws(s); /* FWS */ + if (!*s) break; +/* debug_printf("%s: consider '%s'\n", __FUNCTION__, s); */ + tagchar = *s++; + s = skip_fws(s); /* FWS */ + if (!*s) break; + + if (!instance_only || tagchar == 'i') switch (tagchar) + { + case 'a': /* a= AMS algorithm */ + { + if (*s != '=') return US"no 'a' value"; + if (arc_insert_tagvalue(al, offsetof(arc_line, a), &s)) return US"a tag dup"; + + /* substructure: algo-hash (eg. rsa-sha256) */ + + t = al->a_algo.data = al->a.data; + while (*t != '-') + if (!*t++ || ++i > al->a.len) return US"no '-' in 'a' value"; + al->a_algo.len = i; + if (*t++ != '-') return US"no '-' in 'a' value"; + al->a_hash.data = t; + al->a_hash.len = al->a.len - i - 1; + } + break; + case 'b': + { + gstring * g = NULL; + + switch (*s) + { + case '=': /* b= AMS signature */ + if (al->b.data) return US"already b data"; + bstart = s+1; + + /* The signature can have FWS inserted in the content; + make a stripped copy */ + + while ((c = *++s) && c != ';') + if (c != ' ' && c != '\t' && c != '\n' && c != '\r') + g = string_catn(g, s, 1); + if (!g) return US"no b= value"; + al->b.data = string_from_gstring(g); + al->b.len = g->ptr; + gstring_release_unused(g); + bend = s; + break; + case 'h': /* bh= AMS body hash */ + s = skip_fws(++s); /* FWS */ + if (*s != '=') return US"no bh value"; + if (al->bh.data) return US"already bh data"; + + /* The bodyhash can have FWS inserted in the content; + make a stripped copy */ + + while ((c = *++s) && c != ';') + if (c != ' ' && c != '\t' && c != '\n' && c != '\r') + g = string_catn(g, s, 1); + if (!g) return US"no bh= value"; + al->bh.data = string_from_gstring(g); + al->bh.len = g->ptr; + gstring_release_unused(g); + break; + default: + return US"b? tag"; + } + } + break; + case 'c': + switch (*s) + { + case '=': /* c= AMS canonicalisation */ + if (arc_insert_tagvalue(al, offsetof(arc_line, c), &s)) return US"c tag dup"; + + /* substructure: head/body (eg. relaxed/simple)) */ + + t = al->c_head.data = al->c.data; + while (isalpha(*t)) + if (!*t++ || ++i > al->a.len) break; + al->c_head.len = i; + if (*t++ == '/') /* /body is optional */ + { + al->c_body.data = t; + al->c_body.len = al->c.len - i - 1; + } + else + { + al->c_body.data = US"simple"; + al->c_body.len = 6; + } + break; + case 'v': /* cv= AS validity */ + if (*++s != '=') return US"cv tag val"; + if (arc_insert_tagvalue(al, offsetof(arc_line, cv), &s)) return US"cv tag dup"; + break; + default: + return US"c? tag"; + } + break; + case 'd': /* d= AMS domain */ + if (*s != '=') return US"d tag val"; + if (arc_insert_tagvalue(al, offsetof(arc_line, d), &s)) return US"d tag dup"; + break; + case 'h': /* h= AMS headers */ + if (*s != '=') return US"h tag val"; + if (arc_insert_tagvalue(al, offsetof(arc_line, h), &s)) return US"h tag dup"; + break; + case 'i': /* i= ARC set instance */ + if (*s != '=') return US"i tag val"; + if (arc_insert_tagvalue(al, offsetof(arc_line, i), &s)) return US"i tag dup"; + if (instance_only) goto done; + break; + case 'l': /* l= bodylength */ + if (*s != '=') return US"l tag val"; + if (arc_insert_tagvalue(al, offsetof(arc_line, l), &s)) return US"l tag dup"; + break; + case 's': /* s= AMS selector */ + if (*s != '=') return US"s tag val"; + if (arc_insert_tagvalue(al, offsetof(arc_line, s), &s)) return US"s tag dup"; + break; + } + + while ((c = *s) && c != ';') s++; + if (c) s++; /* ; after tag-spec */ + + /* for all but the b= tag, copy the field including FWS. For the b=, + drop the tag content. */ + + if (!instance_only) + if (bstart) + { + size_t n = bstart - fieldstart; + memcpy(r, fieldstart, n); /* FWS "b=" */ + r += n; + al->rawsig_no_b_val.len += n; + n = s - bend; + memcpy(r, bend, n); /* FWS ";" */ + r += n; + al->rawsig_no_b_val.len += n; + } + else + { + size_t n = s - fieldstart; + memcpy(r, fieldstart, n); + r += n; + al->rawsig_no_b_val.len += n; + } + } + +if (!instance_only) + *r = '\0'; + +done: +/* debug_printf("%s: finshed\n", __FUNCTION__); */ +return NULL; +} + + +/* Insert one header line in the correct set of the chain, +adding instances as needed and checking for duplicate lines. +*/ + +static uschar * +arc_insert_hdr(arc_ctx * ctx, header_line * h, unsigned off, unsigned hoff, + BOOL instance_only, arc_line ** alp_ret) +{ +unsigned i; +arc_set * as; +arc_line * al = store_get(sizeof(arc_line), GET_UNTAINTED), ** alp; +uschar * e; + +memset(al, 0, sizeof(arc_line)); + +if ((e = arc_parse_line(al, h, off, instance_only))) + { + DEBUG(D_acl) if (e) debug_printf("ARC: %s\n", e); + return US"line parse"; + } +if (!(i = arc_instance_from_hdr(al))) return US"instance find"; +if (i > 50) return US"overlarge instance number"; +if (!(as = arc_find_set(ctx, i))) return US"set find"; +if (*(alp = (arc_line **)(US as + hoff))) return US"dup hdr"; + +*alp = al; +if (alp_ret) *alp_ret = al; +return NULL; +} + + + + +static const uschar * +arc_try_header(arc_ctx * ctx, header_line * h, BOOL instance_only) +{ +const uschar * e; + +/*debug_printf("consider hdr '%s'\n", h->text);*/ +if (strncmpic(ARC_HDR_AAR, h->text, ARC_HDRLEN_AAR) == 0) + { + DEBUG(D_acl) + { + int len = h->slen; + uschar * s; + for (s = h->text + h->slen; s[-1] == '\r' || s[-1] == '\n'; ) + s--, len--; + debug_printf("ARC: found AAR: %.*s\n", len, h->text); + } + if ((e = arc_insert_hdr(ctx, h, ARC_HDRLEN_AAR, offsetof(arc_set, hdr_aar), + TRUE, NULL))) + { + DEBUG(D_acl) debug_printf("inserting AAR: %s\n", e); + return US"inserting AAR"; + } + } +else if (strncmpic(ARC_HDR_AMS, h->text, ARC_HDRLEN_AMS) == 0) + { + arc_line * ams; + + DEBUG(D_acl) + { + int len = h->slen; + uschar * s; + for (s = h->text + h->slen; s[-1] == '\r' || s[-1] == '\n'; ) + s--, len--; + debug_printf("ARC: found AMS: %.*s\n", len, h->text); + } + if ((e = arc_insert_hdr(ctx, h, ARC_HDRLEN_AMS, offsetof(arc_set, hdr_ams), + instance_only, &ams))) + { + DEBUG(D_acl) debug_printf("inserting AMS: %s\n", e); + return US"inserting AMS"; + } + + /* defaults */ + if (!ams->c.data) + { + ams->c_head.data = US"simple"; ams->c_head.len = 6; + ams->c_body = ams->c_head; + } + } +else if (strncmpic(ARC_HDR_AS, h->text, ARC_HDRLEN_AS) == 0) + { + DEBUG(D_acl) + { + int len = h->slen; + uschar * s; + for (s = h->text + h->slen; s[-1] == '\r' || s[-1] == '\n'; ) + s--, len--; + debug_printf("ARC: found AS: %.*s\n", len, h->text); + } + if ((e = arc_insert_hdr(ctx, h, ARC_HDRLEN_AS, offsetof(arc_set, hdr_as), + instance_only, NULL))) + { + DEBUG(D_acl) debug_printf("inserting AS: %s\n", e); + return US"inserting AS"; + } + } +return NULL; +} + + + +/* Gather the chain of arc sets from the headers. +Check for duplicates while that is done. Also build the +reverse-order headers list; + +Return: ARC state if determined, eg. by lack of any ARC chain. +*/ + +static const uschar * +arc_vfy_collect_hdrs(arc_ctx * ctx) +{ +header_line * h; +hdr_rlist * r = NULL, * rprev = NULL; +const uschar * e; + +DEBUG(D_acl) debug_printf("ARC: collecting arc sets\n"); +for (h = header_list; h; h = h->next) + { + r = store_get(sizeof(hdr_rlist), GET_UNTAINTED); + r->prev = rprev; + r->used = FALSE; + r->h = h; + rprev = r; + + if ((e = arc_try_header(ctx, h, FALSE))) + { + arc_state_reason = string_sprintf("collecting headers: %s", e); + return US"fail"; + } + } +headers_rlist = r; + +if (!ctx->arcset_chain) return US"none"; +return NULL; +} + + +static BOOL +arc_cv_match(arc_line * al, const uschar * s) +{ +return Ustrncmp(s, al->cv.data, al->cv.len) == 0; +} + +/******************************************************************************/ + +/* Return the hash of headers from the message that the AMS claims it +signed. +*/ + +static void +arc_get_verify_hhash(arc_ctx * ctx, arc_line * ams, blob * hhash) +{ +const uschar * headernames = string_copyn(ams->h.data, ams->h.len); +const uschar * hn; +int sep = ':'; +hdr_rlist * r; +BOOL relaxed = Ustrncmp(US"relaxed", ams->c_head.data, ams->c_head.len) == 0; +int hashtype = pdkim_hashname_to_hashtype( + ams->a_hash.data, ams->a_hash.len); +hctx hhash_ctx; +const uschar * s; +int len; + +if ( hashtype == -1 + || !exim_sha_init(&hhash_ctx, pdkim_hashes[hashtype].exim_hashmethod)) + { + DEBUG(D_acl) + debug_printf("ARC: hash setup error, possibly nonhandled hashtype\n"); + return; + } + +/* For each headername in the list from the AMS (walking in order) +walk the message headers in reverse order, adding to the hash any +found for the first time. For that last point, maintain used-marks +on the list of message headers. */ + +DEBUG(D_acl) debug_printf("ARC: AMS header data for verification:\n"); + +for (r = headers_rlist; r; r = r->prev) + r->used = FALSE; +while ((hn = string_nextinlist(&headernames, &sep, NULL, 0))) + for (r = headers_rlist; r; r = r->prev) + if ( !r->used + && strncasecmp(CCS (s = r->h->text), CCS hn, Ustrlen(hn)) == 0 + ) + { + if (relaxed) s = pdkim_relax_header_n(s, r->h->slen, TRUE); + + len = Ustrlen(s); + DEBUG(D_acl) pdkim_quoteprint(s, len); + exim_sha_update_string(&hhash_ctx, s); + r->used = TRUE; + break; + } + +/* Finally add in the signature header (with the b= tag stripped); no CRLF */ + +s = ams->rawsig_no_b_val.data, len = ams->rawsig_no_b_val.len; +if (relaxed) + len = Ustrlen(s = pdkim_relax_header_n(s, len, FALSE)); +DEBUG(D_acl) pdkim_quoteprint(s, len); +exim_sha_update(&hhash_ctx, s, len); + +exim_sha_finish(&hhash_ctx, hhash); +DEBUG(D_acl) + { debug_printf("ARC: header hash: "); pdkim_hexprint(hhash->data, hhash->len); } +return; +} + + + + +static pdkim_pubkey * +arc_line_to_pubkey(arc_line * al) +{ +uschar * dns_txt; +pdkim_pubkey * p; + +if (!(dns_txt = dkim_exim_query_dns_txt(string_sprintf("%.*s._domainkey.%.*s", + (int)al->s.len, al->s.data, (int)al->d.len, al->d.data)))) + { + DEBUG(D_acl) debug_printf("pubkey dns lookup fail\n"); + return NULL; + } + +if ( !(p = pdkim_parse_pubkey_record(dns_txt)) + || (Ustrcmp(p->srvtype, "*") != 0 && Ustrcmp(p->srvtype, "email") != 0) + ) + { + DEBUG(D_acl) debug_printf("pubkey dns lookup format error\n"); + return NULL; + } + +/* If the pubkey limits use to specified hashes, reject unusable +signatures. XXX should we have looked for multiple dns records? */ + +if (p->hashes) + { + const uschar * list = p->hashes, * ele; + int sep = ':'; + + while ((ele = string_nextinlist(&list, &sep, NULL, 0))) + if (Ustrncmp(ele, al->a_hash.data, al->a_hash.len) == 0) break; + if (!ele) + { + DEBUG(D_acl) debug_printf("pubkey h=%s vs sig a=%.*s\n", + p->hashes, (int)al->a.len, al->a.data); + return NULL; + } + } +return p; +} + + + + +static pdkim_bodyhash * +arc_ams_setup_vfy_bodyhash(arc_line * ams) +{ +int canon_head = -1, canon_body = -1; +long bodylen; + +if (!ams->c.data) ams->c.data = US"simple"; /* RFC 6376 (DKIM) default */ +pdkim_cstring_to_canons(ams->c.data, ams->c.len, &canon_head, &canon_body); +bodylen = ams->l.data + ? strtol(CS string_copyn(ams->l.data, ams->l.len), NULL, 10) : -1; + +return pdkim_set_bodyhash(dkim_verify_ctx, + pdkim_hashname_to_hashtype(ams->a_hash.data, ams->a_hash.len), + canon_body, + bodylen); +} + + + +/* Verify an AMS. This is a DKIM-sig header, but with an ARC i= tag +and without a DKIM v= tag. +*/ + +static const uschar * +arc_ams_verify(arc_ctx * ctx, arc_set * as) +{ +arc_line * ams = as->hdr_ams; +pdkim_bodyhash * b; +pdkim_pubkey * p; +blob sighash; +blob hhash; +ev_ctx vctx; +int hashtype; +const uschar * errstr; + +as->ams_verify_done = US"in-progress"; + +/* Check the AMS has all the required tags: + "a=" algorithm + "b=" signature + "bh=" body hash + "d=" domain (for key lookup) + "h=" headers (included in signature) + "s=" key-selector (for key lookup) +*/ +if ( !ams->a.data || !ams->b.data || !ams->bh.data || !ams->d.data + || !ams->h.data || !ams->s.data) + { + as->ams_verify_done = arc_state_reason = US"required tag missing"; + return US"fail"; + } + + +/* The bodyhash should have been created earlier, and the dkim code should +have managed calculating it during message input. Find the reference to it. */ + +if (!(b = arc_ams_setup_vfy_bodyhash(ams))) + { + as->ams_verify_done = arc_state_reason = US"internal hash setup error"; + return US"fail"; + } + +DEBUG(D_acl) + { + debug_printf("ARC i=%d AMS Body bytes hashed: %lu\n" + " Body %.*s computed: ", + as->instance, b->signed_body_bytes, + (int)ams->a_hash.len, ams->a_hash.data); + pdkim_hexprint(CUS b->bh.data, b->bh.len); + } + +/* We know the bh-tag blob is of a nul-term string, so safe as a string */ + +if ( !ams->bh.data + || (pdkim_decode_base64(ams->bh.data, &sighash), sighash.len != b->bh.len) + || memcmp(sighash.data, b->bh.data, b->bh.len) != 0 + ) + { + DEBUG(D_acl) + { + debug_printf("ARC i=%d AMS Body hash from headers: ", as->instance); + pdkim_hexprint(sighash.data, sighash.len); + debug_printf("ARC i=%d AMS Body hash did NOT match\n", as->instance); + } + return as->ams_verify_done = arc_state_reason = US"AMS body hash miscompare"; + } + +DEBUG(D_acl) debug_printf("ARC i=%d AMS Body hash compared OK\n", as->instance); + +/* Get the public key from DNS */ + +if (!(p = arc_line_to_pubkey(ams))) + return as->ams_verify_done = arc_state_reason = US"pubkey problem"; + +/* We know the b-tag blob is of a nul-term string, so safe as a string */ +pdkim_decode_base64(ams->b.data, &sighash); + +arc_get_verify_hhash(ctx, ams, &hhash); + +/* Setup the interface to the signing library */ + +if ((errstr = exim_dkim_verify_init(&p->key, KEYFMT_DER, &vctx, NULL))) + { + DEBUG(D_acl) debug_printf("ARC verify init: %s\n", errstr); + as->ams_verify_done = arc_state_reason = US"internal sigverify init error"; + return US"fail"; + } + +hashtype = pdkim_hashname_to_hashtype(ams->a_hash.data, ams->a_hash.len); +if (hashtype == -1) + { + DEBUG(D_acl) debug_printf("ARC i=%d AMS verify bad a_hash\n", as->instance); + return as->ams_verify_done = arc_state_reason = US"AMS sig nonverify"; + } + +if ((errstr = exim_dkim_verify(&vctx, + pdkim_hashes[hashtype].exim_hashmethod, &hhash, &sighash))) + { + DEBUG(D_acl) debug_printf("ARC i=%d AMS verify %s\n", as->instance, errstr); + return as->ams_verify_done = arc_state_reason = US"AMS sig nonverify"; + } + +DEBUG(D_acl) debug_printf("ARC i=%d AMS verify pass\n", as->instance); +as->ams_verify_passed = TRUE; +return NULL; +} + + + +/* Check the sets are instance-continuous and that all +members are present. Check that no arc_seals are "fail". +Set the highest instance number global. +Verify the latest AMS. +*/ +static uschar * +arc_headers_check(arc_ctx * ctx) +{ +arc_set * as; +int inst; +BOOL ams_fail_found = FALSE; + +if (!(as = ctx->arcset_chain_last)) + return US"none"; + +for(inst = as->instance; as; as = as->prev, inst--) + { + if (as->instance != inst) + arc_state_reason = string_sprintf("i=%d (sequence; expected %d)", + as->instance, inst); + else if (!as->hdr_aar || !as->hdr_ams || !as->hdr_as) + arc_state_reason = string_sprintf("i=%d (missing header)", as->instance); + else if (arc_cv_match(as->hdr_as, US"fail")) + arc_state_reason = string_sprintf("i=%d (cv)", as->instance); + else + goto good; + + DEBUG(D_acl) debug_printf("ARC chain fail at %s\n", arc_state_reason); + return US"fail"; + + good: + /* Evaluate the oldest-pass AMS validation while we're here. + It does not affect the AS chain validation but is reported as + auxilary info. */ + + if (!ams_fail_found) + if (arc_ams_verify(ctx, as)) + ams_fail_found = TRUE; + else + arc_oldest_pass = inst; + arc_state_reason = NULL; + } +if (inst != 0) + { + arc_state_reason = string_sprintf("(sequence; expected i=%d)", inst); + DEBUG(D_acl) debug_printf("ARC chain fail %s\n", arc_state_reason); + return US"fail"; + } + +arc_received = ctx->arcset_chain_last; +arc_received_instance = arc_received->instance; + +/* We can skip the latest-AMS validation, if we already did it. */ + +as = ctx->arcset_chain_last; +if (!as->ams_verify_passed) + { + if (as->ams_verify_done) + { + arc_state_reason = as->ams_verify_done; + return US"fail"; + } + if (!!arc_ams_verify(ctx, as)) + return US"fail"; + } +return NULL; +} + + +/******************************************************************************/ +static const uschar * +arc_seal_verify(arc_ctx * ctx, arc_set * as) +{ +arc_line * hdr_as = as->hdr_as; +arc_set * as2; +int hashtype; +hctx hhash_ctx; +blob hhash_computed; +blob sighash; +ev_ctx vctx; +pdkim_pubkey * p; +const uschar * errstr; + +DEBUG(D_acl) debug_printf("ARC: AS vfy i=%d\n", as->instance); +/* + 1. If the value of the "cv" tag on that seal is "fail", the + chain state is "fail" and the algorithm stops here. (This + step SHOULD be skipped if the earlier step (2.1) was + performed) [it was] + + 2. In Boolean nomenclature: if ((i == 1 && cv != "none") or (cv + == "none" && i != 1)) then the chain state is "fail" and the + algorithm stops here (note that the ordering of the logic is + structured for short-circuit evaluation). +*/ + +if ( as->instance == 1 && !arc_cv_match(hdr_as, US"none") + || arc_cv_match(hdr_as, US"none") && as->instance != 1 + ) + { + arc_state_reason = US"seal cv state"; + return US"fail"; + } + +/* + 3. Initialize a hash function corresponding to the "a" tag of + the ARC-Seal. +*/ + +hashtype = pdkim_hashname_to_hashtype(hdr_as->a_hash.data, hdr_as->a_hash.len); + +if ( hashtype == -1 + || !exim_sha_init(&hhash_ctx, pdkim_hashes[hashtype].exim_hashmethod)) + { + DEBUG(D_acl) + debug_printf("ARC: hash setup error, possibly nonhandled hashtype\n"); + arc_state_reason = US"seal hash setup error"; + return US"fail"; + } + +/* + 4. Compute the canonicalized form of the ARC header fields, in + the order described in Section 5.4.2, using the "relaxed" + header canonicalization defined in Section 3.4.2 of + [RFC6376]. Pass the canonicalized result to the hash + function. + +Headers are CRLF-separated, but the last one is not crlf-terminated. +*/ + +DEBUG(D_acl) debug_printf("ARC: AS header data for verification:\n"); +for (as2 = ctx->arcset_chain; + as2 && as2->instance <= as->instance; + as2 = as2->next) + { + arc_line * al; + uschar * s; + int len; + + al = as2->hdr_aar; + if (!(s = al->relaxed)) + al->relaxed = s = pdkim_relax_header_n(al->complete->text, + al->complete->slen, TRUE); + len = Ustrlen(s); + DEBUG(D_acl) pdkim_quoteprint(s, len); + exim_sha_update(&hhash_ctx, s, len); + + al = as2->hdr_ams; + if (!(s = al->relaxed)) + al->relaxed = s = pdkim_relax_header_n(al->complete->text, + al->complete->slen, TRUE); + len = Ustrlen(s); + DEBUG(D_acl) pdkim_quoteprint(s, len); + exim_sha_update(&hhash_ctx, s, len); + + al = as2->hdr_as; + if (as2->instance == as->instance) + s = pdkim_relax_header_n(al->rawsig_no_b_val.data, + al->rawsig_no_b_val.len, FALSE); + else if (!(s = al->relaxed)) + al->relaxed = s = pdkim_relax_header_n(al->complete->text, + al->complete->slen, TRUE); + len = Ustrlen(s); + DEBUG(D_acl) pdkim_quoteprint(s, len); + exim_sha_update(&hhash_ctx, s, len); + } + +/* + 5. Retrieve the final digest from the hash function. +*/ + +exim_sha_finish(&hhash_ctx, &hhash_computed); +DEBUG(D_acl) + { + debug_printf("ARC i=%d AS Header %.*s computed: ", + as->instance, (int)hdr_as->a_hash.len, hdr_as->a_hash.data); + pdkim_hexprint(hhash_computed.data, hhash_computed.len); + } + + +/* + 6. Retrieve the public key identified by the "s" and "d" tags in + the ARC-Seal, as described in Section 4.1.6. +*/ + +if (!(p = arc_line_to_pubkey(hdr_as))) + return US"pubkey problem"; + +/* + 7. Determine whether the signature portion ("b" tag) of the ARC- + Seal and the digest computed above are valid according to the + public key. (See also Section Section 8.4 for failure case + handling) + + 8. If the signature is not valid, the chain state is "fail" and + the algorithm stops here. +*/ + +/* We know the b-tag blob is of a nul-term string, so safe as a string */ +pdkim_decode_base64(hdr_as->b.data, &sighash); + +if ((errstr = exim_dkim_verify_init(&p->key, KEYFMT_DER, &vctx, NULL))) + { + DEBUG(D_acl) debug_printf("ARC verify init: %s\n", errstr); + return US"fail"; + } + +if ((errstr = exim_dkim_verify(&vctx, + pdkim_hashes[hashtype].exim_hashmethod, + &hhash_computed, &sighash))) + { + DEBUG(D_acl) + debug_printf("ARC i=%d AS headers verify: %s\n", as->instance, errstr); + arc_state_reason = US"seal sigverify error"; + return US"fail"; + } + +DEBUG(D_acl) debug_printf("ARC: AS vfy i=%d pass\n", as->instance); +return NULL; +} + + +static const uschar * +arc_verify_seals(arc_ctx * ctx) +{ +arc_set * as = ctx->arcset_chain_last; + +if (!as) + return US"none"; + +for ( ; as; as = as->prev) if (arc_seal_verify(ctx, as)) return US"fail"; + +DEBUG(D_acl) debug_printf("ARC: AS vfy overall pass\n"); +return NULL; +} +/******************************************************************************/ + +/* Do ARC verification. Called from DATA ACL, on a verify = arc +condition. No arguments; we are checking globals. + +Return: The ARC state, or NULL on error. +*/ + +const uschar * +acl_verify_arc(void) +{ +const uschar * res; + +memset(&arc_verify_ctx, 0, sizeof(arc_verify_ctx)); + +if (!dkim_verify_ctx) + { + DEBUG(D_acl) debug_printf("ARC: no DKIM verify context\n"); + return NULL; + } + +/* AS evaluation, per +https://tools.ietf.org/html/draft-ietf-dmarc-arc-protocol-10#section-6 +*/ +/* 1. Collect all ARC sets currently on the message. If there were + none, the ARC state is "none" and the algorithm stops here. +*/ + +if ((res = arc_vfy_collect_hdrs(&arc_verify_ctx))) + goto out; + +/* 2. If the form of any ARC set is invalid (e.g., does not contain + exactly one of each of the three ARC-specific header fields), + then the chain state is "fail" and the algorithm stops here. + + 1. To avoid the overhead of unnecessary computation and delay + from crypto and DNS operations, the cv value for all ARC- + Seal(s) MAY be checked at this point. If any of the values + are "fail", then the overall state of the chain is "fail" and + the algorithm stops here. + + 3. Conduct verification of the ARC-Message-Signature header field + bearing the highest instance number. If this verification fails, + then the chain state is "fail" and the algorithm stops here. +*/ + +if ((res = arc_headers_check(&arc_verify_ctx))) + goto out; + +/* 4. For each ARC-Seal from the "N"th instance to the first, apply the + following logic: + + 1. If the value of the "cv" tag on that seal is "fail", the + chain state is "fail" and the algorithm stops here. (This + step SHOULD be skipped if the earlier step (2.1) was + performed) + + 2. In Boolean nomenclature: if ((i == 1 && cv != "none") or (cv + == "none" && i != 1)) then the chain state is "fail" and the + algorithm stops here (note that the ordering of the logic is + structured for short-circuit evaluation). + + 3. Initialize a hash function corresponding to the "a" tag of + the ARC-Seal. + + 4. Compute the canonicalized form of the ARC header fields, in + the order described in Section 5.4.2, using the "relaxed" + header canonicalization defined in Section 3.4.2 of + [RFC6376]. Pass the canonicalized result to the hash + function. + + 5. Retrieve the final digest from the hash function. + + 6. Retrieve the public key identified by the "s" and "d" tags in + the ARC-Seal, as described in Section 4.1.6. + + 7. Determine whether the signature portion ("b" tag) of the ARC- + Seal and the digest computed above are valid according to the + public key. (See also Section Section 8.4 for failure case + handling) + + 8. If the signature is not valid, the chain state is "fail" and + the algorithm stops here. + + 5. If all seals pass validation, then the chain state is "pass", and + the algorithm is complete. +*/ + +if ((res = arc_verify_seals(&arc_verify_ctx))) + goto out; + +res = US"pass"; + +out: + return res; +} + +/******************************************************************************/ + +/* Prepend the header to the rlist */ + +static hdr_rlist * +arc_rlist_entry(hdr_rlist * list, const uschar * s, int len) +{ +hdr_rlist * r = store_get(sizeof(hdr_rlist) + sizeof(header_line), GET_UNTAINTED); +header_line * h = r->h = (header_line *)(r+1); + +r->prev = list; +r->used = FALSE; +h->next = NULL; +h->type = 0; +h->slen = len; +h->text = US s; + +return r; +} + + +/* Walk the given headers strings identifying each header, and construct +a reverse-order list. +*/ + +static hdr_rlist * +arc_sign_scan_headers(arc_ctx * ctx, gstring * sigheaders) +{ +const uschar * s; +hdr_rlist * rheaders = NULL; + +s = sigheaders ? sigheaders->s : NULL; +if (s) while (*s) + { + const uschar * s2 = s; + + /* This works for either NL or CRLF lines; also nul-termination */ + while (*++s2) + if (*s2 == '\n' && s2[1] != '\t' && s2[1] != ' ') break; + s2++; /* move past end of line */ + + rheaders = arc_rlist_entry(rheaders, s, s2 - s); + s = s2; + } +return rheaders; +} + + + +/* Return the A-R content, without identity, with line-ending and +NUL termination. */ + +static BOOL +arc_sign_find_ar(header_line * headers, const uschar * identity, blob * ret) +{ +header_line * h; +int ilen = Ustrlen(identity); + +ret->data = NULL; +for(h = headers; h; h = h->next) + { + uschar * s = h->text, c; + int len = h->slen; + + if (Ustrncmp(s, HDR_AR, HDRLEN_AR) != 0) continue; + s += HDRLEN_AR, len -= HDRLEN_AR; /* header name */ + while ( len > 0 + && (c = *s) && (c == ' ' || c == '\t' || c == '\r' || c == '\n')) + s++, len--; /* FWS */ + if (Ustrncmp(s, identity, ilen) != 0) continue; + s += ilen; len -= ilen; /* identity */ + if (len <= 0) continue; + if ((c = *s) && c == ';') s++, len--; /* identity terminator */ + while ( len > 0 + && (c = *s) && (c == ' ' || c == '\t' || c == '\r' || c == '\n')) + s++, len--; /* FWS */ + if (len <= 0) continue; + ret->data = s; + ret->len = len; + return TRUE; + } +return FALSE; +} + + + +/* Append a constructed AAR including CRLF. Add it to the arc_ctx too. */ + +static gstring * +arc_sign_append_aar(gstring * g, arc_ctx * ctx, + const uschar * identity, int instance, blob * ar) +{ +int aar_off = gstring_length(g); +arc_set * as = + store_get(sizeof(arc_set) + sizeof(arc_line) + sizeof(header_line), GET_UNTAINTED); +arc_line * al = (arc_line *)(as+1); +header_line * h = (header_line *)(al+1); + +g = string_catn(g, ARC_HDR_AAR, ARC_HDRLEN_AAR); +g = string_fmt_append(g, " i=%d; %s;\r\n\t", instance, identity); +g = string_catn(g, US ar->data, ar->len); + +h->slen = g->ptr - aar_off; +h->text = g->s + aar_off; +al->complete = h; +as->next = NULL; +as->prev = ctx->arcset_chain_last; +as->instance = instance; +as->hdr_aar = al; +if (instance == 1) + ctx->arcset_chain = as; +else + ctx->arcset_chain_last->next = as; +ctx->arcset_chain_last = as; + +DEBUG(D_transport) debug_printf("ARC: AAR '%.*s'\n", h->slen - 2, h->text); +return g; +} + + + +static BOOL +arc_sig_from_pseudoheader(gstring * hdata, int hashtype, const uschar * privkey, + blob * sig, const uschar * why) +{ +hashmethod hm = /*sig->keytype == KEYTYPE_ED25519*/ FALSE + ? HASH_SHA2_512 : pdkim_hashes[hashtype].exim_hashmethod; +blob hhash; +es_ctx sctx; +const uschar * errstr; + +DEBUG(D_transport) + { + hctx hhash_ctx; + debug_printf("ARC: %s header data for signing:\n", why); + pdkim_quoteprint(hdata->s, hdata->ptr); + + (void) exim_sha_init(&hhash_ctx, pdkim_hashes[hashtype].exim_hashmethod); + exim_sha_update(&hhash_ctx, hdata->s, hdata->ptr); + exim_sha_finish(&hhash_ctx, &hhash); + debug_printf("ARC: header hash: "); pdkim_hexprint(hhash.data, hhash.len); + } + +if (FALSE /*need hash for Ed25519 or GCrypt signing*/ ) + { + hctx hhash_ctx; + (void) exim_sha_init(&hhash_ctx, pdkim_hashes[hashtype].exim_hashmethod); + exim_sha_update(&hhash_ctx, hdata->s, hdata->ptr); + exim_sha_finish(&hhash_ctx, &hhash); + } +else + { + hhash.data = hdata->s; + hhash.len = hdata->ptr; + } + +if ( (errstr = exim_dkim_signing_init(privkey, &sctx)) + || (errstr = exim_dkim_sign(&sctx, hm, &hhash, sig))) + { + log_write(0, LOG_MAIN, "ARC: %s signing: %s\n", why, errstr); + DEBUG(D_transport) + debug_printf("private key, or private-key file content, was: '%s'\n", + privkey); + return FALSE; + } +return TRUE; +} + + + +static gstring * +arc_sign_append_sig(gstring * g, blob * sig) +{ +/*debug_printf("%s: raw sig ", __FUNCTION__); pdkim_hexprint(sig->data, sig->len);*/ +sig->data = pdkim_encode_base64(sig); +sig->len = Ustrlen(sig->data); +for (;;) + { + int len = MIN(sig->len, 74); + g = string_catn(g, sig->data, len); + if ((sig->len -= len) == 0) break; + sig->data += len; + g = string_catn(g, US"\r\n\t ", 5); + } +g = string_catn(g, US";\r\n", 3); +gstring_release_unused(g); +string_from_gstring(g); +return g; +} + + +/* Append a constructed AMS including CRLF. Add it to the arc_ctx too. */ + +static gstring * +arc_sign_append_ams(gstring * g, arc_ctx * ctx, int instance, + const uschar * identity, const uschar * selector, blob * bodyhash, + hdr_rlist * rheaders, const uschar * privkey, unsigned options) +{ +uschar * s; +gstring * hdata = NULL; +int col; +int hashtype = pdkim_hashname_to_hashtype(US"sha256", 6); /*XXX hardwired */ +blob sig; +int ams_off; +arc_line * al = store_get(sizeof(header_line) + sizeof(arc_line), GET_UNTAINTED); +header_line * h = (header_line *)(al+1); + +/* debug_printf("%s\n", __FUNCTION__); */ + +/* Construct the to-be-signed AMS pseudo-header: everything but the sig. */ + +ams_off = g->ptr; +g = string_fmt_append(g, "%s i=%d; a=rsa-sha256; c=relaxed; d=%s; s=%s", + ARC_HDR_AMS, instance, identity, selector); /*XXX hardwired a= */ +if (options & ARC_SIGN_OPT_TSTAMP) + g = string_fmt_append(g, "; t=%lu", (u_long)now); +if (options & ARC_SIGN_OPT_EXPIRE) + g = string_fmt_append(g, "; x=%lu", (u_long)expire); +g = string_fmt_append(g, ";\r\n\tbh=%s;\r\n\th=", + pdkim_encode_base64(bodyhash)); + +for(col = 3; rheaders; rheaders = rheaders->prev) + { + const uschar * hnames = US"DKIM-Signature:" PDKIM_DEFAULT_SIGN_HEADERS; + uschar * name, * htext = rheaders->h->text; + int sep = ':'; + + /* Spot headers of interest */ + + while ((name = string_nextinlist(&hnames, &sep, NULL, 0))) + { + int len = Ustrlen(name); + if (strncasecmp(CCS htext, CCS name, len) == 0) + { + /* If too long, fold line in h= field */ + + if (col + len > 78) g = string_catn(g, US"\r\n\t ", 5), col = 3; + + /* Add name to h= list */ + + g = string_catn(g, name, len); + g = string_catn(g, US":", 1); + col += len + 1; + + /* Accumulate header for hashing/signing */ + + hdata = string_cat(hdata, + pdkim_relax_header_n(htext, rheaders->h->slen, TRUE)); /*XXX hardwired */ + break; + } + } + } + +/* Lose the last colon from the h= list */ + +if (g->s[g->ptr - 1] == ':') g->ptr--; + +g = string_catn(g, US";\r\n\tb=;", 7); + +/* Include the pseudo-header in the accumulation */ + +s = pdkim_relax_header_n(g->s + ams_off, g->ptr - ams_off, FALSE); +hdata = string_cat(hdata, s); + +/* Calculate the signature from the accumulation */ +/*XXX does that need further relaxation? there are spaces embedded in the b= strings! */ + +if (!arc_sig_from_pseudoheader(hdata, hashtype, privkey, &sig, US"AMS")) + return NULL; + +/* Lose the trailing semicolon from the psuedo-header, and append the signature +(folded over lines) and termination to complete it. */ + +g->ptr--; +g = arc_sign_append_sig(g, &sig); + +h->slen = g->ptr - ams_off; +h->text = g->s + ams_off; +al->complete = h; +ctx->arcset_chain_last->hdr_ams = al; + +DEBUG(D_transport) debug_printf("ARC: AMS '%.*s'\n", h->slen - 2, h->text); +return g; +} + + + +/* Look for an arc= result in an A-R header blob. We know that its data +happens to be a NUL-term string. */ + +static uschar * +arc_ar_cv_status(blob * ar) +{ +const uschar * resinfo = ar->data; +int sep = ';'; +uschar * methodspec, * s; + +while ((methodspec = string_nextinlist(&resinfo, &sep, NULL, 0))) + if (Ustrncmp(methodspec, US"arc=", 4) == 0) + { + uschar c; + for (s = methodspec += 4; + (c = *s) && c != ';' && c != ' ' && c != '\r' && c != '\n'; ) s++; + return string_copyn(methodspec, s - methodspec); + } +return US"none"; +} + + + +/* Build the AS header and prepend it */ + +static gstring * +arc_sign_prepend_as(gstring * arcset_interim, arc_ctx * ctx, + int instance, const uschar * identity, const uschar * selector, blob * ar, + const uschar * privkey, unsigned options) +{ +gstring * arcset; +uschar * status = arc_ar_cv_status(ar); +arc_line * al = store_get(sizeof(header_line) + sizeof(arc_line), GET_UNTAINTED); +header_line * h = (header_line *)(al+1); +uschar * badline_str; + +gstring * hdata = NULL; +int hashtype = pdkim_hashname_to_hashtype(US"sha256", 6); /*XXX hardwired */ +blob sig; + +/* +- Generate AS + - no body coverage + - no h= tag; implicit coverage + - arc status from A-R + - if fail: + - coverage is just the new ARC set + including self (but with an empty b= in self) + - if non-fail: + - all ARC set headers, set-number order, aar then ams then as, + including self (but with an empty b= in self) +*/ +DEBUG(D_transport) debug_printf("ARC: building AS for status '%s'\n", status); + +/* Construct the AS except for the signature */ + +arcset = string_append(NULL, 9, + ARC_HDR_AS, + US" i=", string_sprintf("%d", instance), + US"; cv=", status, + US"; a=rsa-sha256; d=", identity, /*XXX hardwired */ + US"; s=", selector); /*XXX same as AMS */ +if (options & ARC_SIGN_OPT_TSTAMP) + arcset = string_append(arcset, 2, + US"; t=", string_sprintf("%lu", (u_long)now)); +arcset = string_cat(arcset, + US";\r\n\t b=;"); + +h->slen = arcset->ptr; +h->text = arcset->s; +al->complete = h; +ctx->arcset_chain_last->hdr_as = al; + +/* For any but "fail" chain-verify status, walk the entire chain in order by +instance. For fail, only the new arc-set. Accumulate the elements walked. */ + +for (arc_set * as = Ustrcmp(status, US"fail") == 0 + ? ctx->arcset_chain_last : ctx->arcset_chain; + as; as = as->next) + { + arc_line * l; + /* Accumulate AAR then AMS then AS. Relaxed canonicalisation + is required per standard. */ + + badline_str = US"aar"; + if (!(l = as->hdr_aar)) goto badline; + h = l->complete; + hdata = string_cat(hdata, pdkim_relax_header_n(h->text, h->slen, TRUE)); + badline_str = US"ams"; + if (!(l = as->hdr_ams)) goto badline; + h = l->complete; + hdata = string_cat(hdata, pdkim_relax_header_n(h->text, h->slen, TRUE)); + badline_str = US"as"; + if (!(l = as->hdr_as)) goto badline; + h = l->complete; + hdata = string_cat(hdata, pdkim_relax_header_n(h->text, h->slen, !!as->next)); + } + +/* Calculate the signature from the accumulation */ + +if (!arc_sig_from_pseudoheader(hdata, hashtype, privkey, &sig, US"AS")) + return NULL; + +/* Lose the trailing semicolon */ +arcset->ptr--; +arcset = arc_sign_append_sig(arcset, &sig); +DEBUG(D_transport) debug_printf("ARC: AS '%.*s'\n", arcset->ptr - 2, arcset->s); + +/* Finally, append the AMS and AAR to the new AS */ + +return string_catn(arcset, arcset_interim->s, arcset_interim->ptr); + +badline: + DEBUG(D_transport) + debug_printf("ARC: while building AS, missing %s in chain\n", badline_str); + return NULL; +} + + +/**************************************/ + +/* Return pointer to pdkim_bodyhash for given hash method, creating new +method if needed. +*/ + +void * +arc_ams_setup_sign_bodyhash(void) +{ +int canon_head, canon_body; + +DEBUG(D_transport) debug_printf("ARC: requesting bodyhash\n"); +pdkim_cstring_to_canons(US"relaxed", 7, &canon_head, &canon_body); /*XXX hardwired */ +return pdkim_set_bodyhash(&dkim_sign_ctx, + pdkim_hashname_to_hashtype(US"sha256", 6), /*XXX hardwired */ + canon_body, + -1); +} + + + +void +arc_sign_init(void) +{ +memset(&arc_sign_ctx, 0, sizeof(arc_sign_ctx)); +headers_rlist = NULL; +} + + + +/* A "normal" header line, identified by DKIM processing. These arrive before +the call to arc_sign(), which carries any newly-created DKIM headers - and +those go textually before the normal ones in the message. + +We have to take the feed from DKIM as, in the transport-filter case, the +headers are not in memory at the time of the call to arc_sign(). + +Take a copy of the header and construct a reverse-order list. +Also parse ARC-chain headers and build the chain struct, retaining pointers +into the copies. +*/ + +static const uschar * +arc_header_sign_feed(gstring * g) +{ +uschar * s = string_copyn(g->s, g->ptr); +headers_rlist = arc_rlist_entry(headers_rlist, s, g->ptr); +return arc_try_header(&arc_sign_ctx, headers_rlist->h, TRUE); +} + + + +/* Per RFCs 6376, 7489 the only allowed chars in either an ADMD id +or a selector are ALPHA/DIGGIT/'-'/'.' + +Check, to help catch misconfigurations such as a missing selector +element in the arc_sign list. +*/ + +static BOOL +arc_valid_id(const uschar * s) +{ +for (uschar c; c = *s++; ) + if (!isalnum(c) && c != '-' && c != '.') return FALSE; +return TRUE; +} + + + +/* ARC signing. Called from the smtp transport, if the arc_sign option is set. +The dkim_exim_sign() function has already been called, so will have hashed the +message body for us so long as we requested a hash previously. + +Arguments: + signspec Three-element colon-sep list: identity, selector, privkey. + Optional fourth element: comma-sep list of options. + Already expanded + sigheaders Any signature headers already generated, eg. by DKIM, or NULL + errstr Error string + +Return value + Set of headers to prepend to the message, including the supplied sigheaders + but not the plainheaders. +*/ + +gstring * +arc_sign(const uschar * signspec, gstring * sigheaders, uschar ** errstr) +{ +const uschar * identity, * selector, * privkey, * opts, * s; +unsigned options = 0; +int sep = 0; +header_line * headers; +hdr_rlist * rheaders; +blob ar; +int instance; +gstring * g = NULL; +pdkim_bodyhash * b; + +expire = now = 0; + +/* Parse the signing specification */ + +if (!(identity = string_nextinlist(&signspec, &sep, NULL, 0)) || !*identity) + { s = US"identity"; goto bad_arg_ret; } +if (!(selector = string_nextinlist(&signspec, &sep, NULL, 0)) || !*selector) + { s = US"selector"; goto bad_arg_ret; } +if (!(privkey = string_nextinlist(&signspec, &sep, NULL, 0)) || !*privkey) + { s = US"privkey"; goto bad_arg_ret; } +if (!arc_valid_id(identity)) + { s = US"identity"; goto bad_arg_ret; } +if (!arc_valid_id(selector)) + { s = US"selector"; goto bad_arg_ret; } +if (*privkey == '/' && !(privkey = expand_file_big_buffer(privkey))) + goto ret_sigheaders; + +if ((opts = string_nextinlist(&signspec, &sep, NULL, 0))) + { + int osep = ','; + while ((s = string_nextinlist(&opts, &osep, NULL, 0))) + if (Ustrcmp(s, "timestamps") == 0) + { + options |= ARC_SIGN_OPT_TSTAMP; + if (!now) now = time(NULL); + } + else if (Ustrncmp(s, "expire", 6) == 0) + { + options |= ARC_SIGN_OPT_EXPIRE; + if (*(s += 6) == '=') + if (*++s == '+') + { + if (!(expire = (time_t)atoi(CS ++s))) + expire = ARC_SIGN_DEFAULT_EXPIRE_DELTA; + if (!now) now = time(NULL); + expire += now; + } + else + expire = (time_t)atol(CS s); + else + { + if (!now) now = time(NULL); + expire = now + ARC_SIGN_DEFAULT_EXPIRE_DELTA; + } + } + } + +DEBUG(D_transport) debug_printf("ARC: sign for %s\n", identity); + +/* Make an rlist of any new DKIM headers, then add the "normals" rlist to it. +Then scan the list for an A-R header. */ + +string_from_gstring(sigheaders); +if ((rheaders = arc_sign_scan_headers(&arc_sign_ctx, sigheaders))) + { + hdr_rlist ** rp; + for (rp = &headers_rlist; *rp; ) rp = &(*rp)->prev; + *rp = rheaders; + } + +/* Finally, build a normal-order headers list */ +/*XXX only needed for hunt-the-AR? */ +/*XXX also, we really should be accepting any number of ADMD-matching ARs */ + { + header_line * hnext = NULL; + for (rheaders = headers_rlist; rheaders; + hnext = rheaders->h, rheaders = rheaders->prev) + rheaders->h->next = hnext; + headers = hnext; + } + +if (!(arc_sign_find_ar(headers, identity, &ar))) + { + log_write(0, LOG_MAIN, "ARC: no Authentication-Results header for signing"); + goto ret_sigheaders; + } + +/* We previously built the data-struct for the existing ARC chain, if any, using a headers +feed from the DKIM module. Use that to give the instance number for the ARC set we are +about to build. */ + +DEBUG(D_transport) + if (arc_sign_ctx.arcset_chain_last) + debug_printf("ARC: existing chain highest instance: %d\n", + arc_sign_ctx.arcset_chain_last->instance); + else + debug_printf("ARC: no existing chain\n"); + +instance = arc_sign_ctx.arcset_chain_last ? arc_sign_ctx.arcset_chain_last->instance + 1 : 1; + +/* +- Generate AAR + - copy the A-R; prepend i= & identity +*/ + +g = arc_sign_append_aar(g, &arc_sign_ctx, identity, instance, &ar); + +/* +- Generate AMS + - Looks fairly like a DKIM sig + - Cover all DKIM sig headers as well as the usuals + - ? oversigning? + - Covers the data + - we must have requested a suitable bodyhash previously +*/ + +b = arc_ams_setup_sign_bodyhash(); +g = arc_sign_append_ams(g, &arc_sign_ctx, instance, identity, selector, + &b->bh, headers_rlist, privkey, options); + +/* +- Generate AS + - no body coverage + - no h= tag; implicit coverage + - arc status from A-R + - if fail: + - coverage is just the new ARC set + including self (but with an empty b= in self) + - if non-fail: + - all ARC set headers, set-number order, aar then ams then as, + including self (but with an empty b= in self) +*/ + +if (g) + g = arc_sign_prepend_as(g, &arc_sign_ctx, instance, identity, selector, &ar, + privkey, options); + +/* Finally, append the dkim headers and return the lot. */ + +if (sigheaders) g = string_catn(g, sigheaders->s, sigheaders->ptr); + +out: + if (!g) return string_get(1); + (void) string_from_gstring(g); + gstring_release_unused(g); + return g; + + +bad_arg_ret: + log_write(0, LOG_MAIN, "ARC: bad signing-specification (%s)", s); +ret_sigheaders: + g = sigheaders; + goto out; +} + + +/******************************************************************************/ + +/* Check to see if the line is an AMS and if so, set up to validate it. +Called from the DKIM input processing. This must be done now as the message +body data is hashed during input. + +We call the DKIM code to request a body-hash; it has the facility already +and the hash parameters might be common with other requests. +*/ + +static const uschar * +arc_header_vfy_feed(gstring * g) +{ +header_line h; +arc_line al; +pdkim_bodyhash * b; +uschar * errstr; + +if (!dkim_verify_ctx) return US"no dkim context"; + +if (strncmpic(ARC_HDR_AMS, g->s, ARC_HDRLEN_AMS) != 0) return US"not AMS"; + +DEBUG(D_receive) debug_printf("ARC: spotted AMS header\n"); +/* Parse the AMS header */ + +h.next = NULL; +h.slen = g->size; +h.text = g->s; +memset(&al, 0, sizeof(arc_line)); +if ((errstr = arc_parse_line(&al, &h, ARC_HDRLEN_AMS, FALSE))) + { + DEBUG(D_acl) if (errstr) debug_printf("ARC: %s\n", errstr); + goto badline; + } + +if (!al.a_hash.data) + { + DEBUG(D_acl) debug_printf("ARC: no a_hash from '%.*s'\n", h.slen, h.text); + goto badline; + } + +/* defaults */ +if (!al.c.data) + { + al.c_body.data = US"simple"; al.c_body.len = 6; + al.c_head = al.c_body; + } + +/* Ask the dkim code to calc a bodyhash with those specs */ + +if (!(b = arc_ams_setup_vfy_bodyhash(&al))) + return US"dkim hash setup fail"; + +/* Discard the reference; search again at verify time, knowing that one +should have been created here. */ + +return NULL; + +badline: + return US"line parsing error"; +} + + + +/* A header line has been identified by DKIM processing. + +Arguments: + g Header line + is_vfy TRUE for verify mode or FALSE for signing mode + +Return: + NULL for success, or an error string (probably unused) +*/ + +const uschar * +arc_header_feed(gstring * g, BOOL is_vfy) +{ +return is_vfy ? arc_header_vfy_feed(g) : arc_header_sign_feed(g); +} + + + +/******************************************************************************/ + +/* Construct the list of domains from the ARC chain after validation */ + +uschar * +fn_arc_domains(void) +{ +arc_set * as; +unsigned inst; +gstring * g = NULL; + +for (as = arc_verify_ctx.arcset_chain, inst = 1; as; as = as->next, inst++) + { + arc_line * hdr_as = as->hdr_as; + if (hdr_as) + { + blob * d = &hdr_as->d; + + for (; inst < as->instance; inst++) + g = string_catn(g, US":", 1); + + g = d->data && d->len + ? string_append_listele_n(g, ':', d->data, d->len) + : string_catn(g, US":", 1); + } + else + g = string_catn(g, US":", 1); + } +return g ? g->s : US""; +} + + +/* Construct an Authentication-Results header portion, for the ARC module */ + +gstring * +authres_arc(gstring * g) +{ +if (arc_state) + { + arc_line * highest_ams; + int start = 0; /* Compiler quietening */ + DEBUG(D_acl) start = g->ptr; + + g = string_append(g, 2, US";\n\tarc=", arc_state); + if (arc_received_instance > 0) + { + g = string_fmt_append(g, " (i=%d)", arc_received_instance); + if (arc_state_reason) + g = string_append(g, 3, US"(", arc_state_reason, US")"); + g = string_catn(g, US" header.s=", 10); + highest_ams = arc_received->hdr_ams; + g = string_catn(g, highest_ams->s.data, highest_ams->s.len); + + g = string_fmt_append(g, " arc.oldest-pass=%d", arc_oldest_pass); + + if (sender_host_address) + g = string_append(g, 2, US" smtp.remote-ip=", sender_host_address); + } + else if (arc_state_reason) + g = string_append(g, 3, US" (", arc_state_reason, US")"); + DEBUG(D_acl) debug_printf("ARC: authres '%.*s'\n", + g->ptr - start - 3, g->s + start + 3); + } +else + DEBUG(D_acl) debug_printf("ARC: no authres\n"); +return g; +} + + +# endif /* DISABLE_DKIM */ +#endif /* EXPERIMENTAL_ARC */ +/* vi: aw ai sw=2 + */ diff --git a/src/auths/Makefile b/src/auths/Makefile new file mode 100644 index 0000000..e85b22a --- /dev/null +++ b/src/auths/Makefile @@ -0,0 +1,45 @@ +# Make file for building a library containing all the available authorization +# methods, and calling it auths.a. In addition, there are functions that are +# of general use in several methods; these are in separate modules so they are +# linked in only when needed. This Makefile is called from the main make file, +# after cd'ing to the auths subdirectory. When the relevant AUTH_ macros are +# defined, the equivalent modules herein is not included in the final binary. + +OBJ = auth-spa.o call_pam.o call_pwcheck.o \ + call_radius.o check_serv_cond.o cram_md5.o cyrus_sasl.o dovecot.o \ + external.o get_data.o get_no64_data.o gsasl_exim.o heimdal_gssapi.o \ + plaintext.o pwcheck.o \ + spa.o tls.o xtextdecode.o xtextencode.o + +auths.a: $(OBJ) + @$(RM_COMMAND) -f auths.a + @echo "$(AR) auths.a" + $(FE)$(AR) auths.a $(OBJ) + $(RANLIB) $@ + +.SUFFIXES: .o .c +.c.o:; @echo "$(CC) $*.c" + $(FE)$(CC) -c $(CFLAGS) $(INCLUDE) $*.c + +auth-spa.o: $(HDRS) auth-spa.c +call_pam.o: $(HDRS) call_pam.c +call_pwcheck.o: $(HDRS) call_pwcheck.c pwcheck.h +call_radius.o: $(HDRS) call_radius.c +check_serv_cond.o: $(HDRS) check_serv_cond.c +get_data.o: $(HDRS) get_data.c +get_no64_data.o: $(HDRS) get_no64_data.c +pwcheck.o: $(HDRS) pwcheck.c pwcheck.h +xtextdecode.o: $(HDRS) xtextdecode.c +xtextencode.o: $(HDRS) xtextencode.c + +cram_md5.o: $(HDRS) cram_md5.c cram_md5.h +cyrus_sasl.o: $(HDRS) cyrus_sasl.c cyrus_sasl.h +dovecot.o: $(HDRS) dovecot.c dovecot.h +external.o: $(HDRS) external.c external.h +gsasl_exim.o: $(HDRS) gsasl_exim.c gsasl_exim.h +heimdal_gssapi.o: $(HDRS) heimdal_gssapi.c heimdal_gssapi.h +plaintext.o: $(HDRS) plaintext.c plaintext.h +spa.o: $(HDRS) spa.c spa.h +tls.o: $(HDRS) tls.c tls.h + +# End diff --git a/src/auths/README b/src/auths/README new file mode 100644 index 0000000..66bdcdc --- /dev/null +++ b/src/auths/README @@ -0,0 +1,98 @@ +AUTHS + +The modules in this directory are in support of various authentication +functions. Some of them, such as the base64 encoding/decoding and MD5 +computation, are just functions that might be used by several authentication +mechanisms. Others are the SMTP AUTH mechanisms themselves, included in the +final binary if the relevant AUTH_XXX value is set in Local/Makefile. The +general functions are in separate modules so that they get included in the +final binary only if they are actually called from somewhere. + +GENERAL FUNCTIONS + +The API for each of these functions is documented with the function's code. + + auth_b64encode encode in base 64 + auth_b64decode decode from base 64 + auth_call_pam do PAM authentication (if build with SUPPORT_PAM) + auth_get_data issue SMTP AUTH challenge and read response + auth_xtextencode encode as xtext + auth_xtextdecode decode from xtext + +INTERFACE TO SMTP AUTHENTICATION MECHANISMS + +These are general SASL mechanisms, adapted for use with SMTP. Each +authentication mechanism has three functions, for initialization, server +authentication, and client authentication. + +INITIALIZATION + +The initialization function is called when the configuration is read, and can +check for incomplete or illegal settings. It has one argument, a pointer to the +instance block for this configured mechanism. It must set the flags called +"server" and "client" in the generic auth_instance block to indicate whether +the server and/or client functions are available for this authenticator. +Typically this depends on whether server or client configuration options have +been set, but it is also possible to have an authenticator that has only one of +the server or client functions. The function may not touch big_buffer. + +SERVER AUTHENTICATION + +The second function performs authentication as a server. It receives a pointer +to the instance block, and its second argument is the remainder of the data +from the AUTH command. The numeric variable maximum setting (expand_nmax) is +set to zero, with $0 initialized as unset. The authenticator may set up numeric +variables according to its (old) specification and $auth variables the +preferred ones nowadays; it should leave them set at the end so that they can +be used for the expansion of the generic server_set_id option, which happens +centrally. + +This function has access to the SMTP input and output so that it can write +intermediate responses and read more data if necessary. There is a packaged +function in auth_get_data() which outputs a challenge and reads a response. + +The yield of a server authentication check must be one of: + + OK success + DEFER couldn't complete the check + FAIL authentication failed + CANCELLED authentication forced to fail by "*" response to challenge, + or by certain forced string expansion failures + BAD64 bad base64 data received + UNEXPECTED unexpected data received + +In the case of DEFER, auth_defer_msg should point to an error message. + +CLIENT AUTHENTICATION + +The third function performs authentication as a client. It receives a pointer +to the instance block, and four further arguments: + + The smtp_context item for the connection to the remote host. + + The normal command-reading timeout value. + + A pointer to a buffer, to be used for receiving responses. It is done this + way so that the buffer is available for logging etc. in the calling + function in cases of error. + + The size of the buffer. + +The yield of a client authentication check must be one of: + + OK success + FAIL_SEND error after writing a command; errno is set + FAIL failed after reading a response; + either errno is set (for timeouts, I/O failures) or + the buffer contains the SMTP response line + CANCELLED the client cancelled authentication (often "fail" in expansion) + the buffer may contain a message; if not, *buffer = 0 + ERROR local problem (typically expansion error); message in buffer + +To communicate with the remote host the client should call +smtp_write_command(). If this yields FALSE, the authenticator should return +FAIL. After a successful write, the response is received by a call to +smtp_read_response(), which should use the buffer handed to the client function +as an argument. + +**** diff --git a/src/auths/auth-spa.c b/src/auths/auth-spa.c new file mode 100644 index 0000000..8d886b6 --- /dev/null +++ b/src/auths/auth-spa.c @@ -0,0 +1,1524 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* + * This file provides the necessary methods for authenticating with + * Microsoft's Secure Password Authentication. + + * All the original code used here was torn by Marc Prud'hommeaux out of the + * Samba project (by Andrew Tridgell, Jeremy Allison, and others). + * + * Copyright (c) The Exim Maintainers 2021 + + * Tom Kistner provided additional code, adding spa_build_auth_challenge() to + * support server authentication mode. + + * Mark Lyda provided a patch to solve this problem: + + - Exim is indicating in its Authentication Request message (Type 1) that it + can transmit text in either Unicode or OEM format. + + - Microsoft's SMTP server (smtp.email.msn.com) is responding in its + Challenge message (Type 2) that it will be expecting the OEM format. + + - Exim does not pay attention to the text format requested by Microsoft's + SMTP server and, instead, defaults to using the Unicode format. + + * References: + * http://www.innovation.ch/java/ntlm.html + * http://www.kuro5hin.org/story/2002/4/28/1436/66154 + + * It seems that some systems have existing but different definitions of some + * of the following types. I received a complaint about "int16" causing + * compilation problems. So I (PH) have renamed them all, to be on the safe + * side, by adding 'x' on the end. + + * typedef signed short int16; + * typedef unsigned short uint16; + * typedef unsigned uint32; + * typedef unsigned char uint8; + + * The API is extremely simple: + * 1. Form a SPA authentication request based on the username + * and (optional) domain + * 2. Send the request to the server and get an SPA challenge + * 3. Build the challenge response and send it back. + * + * Example usage is as + * follows: + * +int main (int argc, char ** argv) +{ + SPAAuthRequest request; + SPAAuthChallenge challenge; + SPAAuthResponse response; + char msgbuf[2048]; + char buffer[512]; + char *username, *password, *domain, *challenge_str; + + if (argc < 3) + { + printf ("Usage: %s [SPA Challenge]\n", + argv [0]); + exit (1); + } + + username = argv [1]; + password = argv [2]; + domain = 0; + + spa_build_auth_request (&request, username, domain); + + spa_bits_to_base64 (msgbuf, US &request, + spa_request_length(&request)); + + printf ("SPA Login request for username=%s:\n %s\n", + argv [1], msgbuf); + + if (argc < 4) + { + printf ("Run: %s [NTLM Challenge] " \ + "to complete authenitcation\n", argv [0]); + exit (0); + } + + challenge_str = argv [3]; + + if (spa_base64_to_bits (CS &challenge, sizeof(challenge), + CCS (challenge_str))<0) + { + printf("bad base64 data in challenge: %s\n", challenge_str); + exit (1); + } + + spa_build_auth_response (&challenge, &response, username, password); + spa_bits_to_base64 (msgbuf, US &response, + spa_request_length(&response)); + + printf ("SPA Response to challenge:\n %s\n for " \ + "username=%s, password=%s:\n %s\n", + argv[3], argv [1], argv [2], msgbuf); + return 0; +} + * + * + * All the client code used here was torn by Marc Prud'hommeaux out of the + * Samba project (by Andrew Tridgell, Jeremy Allison, and others). + * Previous comments are below: + */ + +/* + Unix SMB/Netbios implementation. + Version 1.9. + + a partial implementation of DES designed for use in the + SMB authentication protocol + + Copyright (C) Andrew Tridgell 1998 + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +*/ + + +/* NOTES: + + This code makes no attempt to be fast! In fact, it is a very + slow implementation + + This code is NOT a complete DES implementation. It implements only + the minimum necessary for SMB authentication, as used by all SMB + products (including every copy of Microsoft Windows95 ever sold) + + In particular, it can only do a unchained forward DES pass. This + means it is not possible to use this code for encryption/decryption + of data, instead it is only useful as a "hash" algorithm. + + There is no entry point into this code that allows normal DES operation. + + I believe this means that this code does not come under ITAR + regulations but this is NOT a legal opinion. If you are concerned + about the applicability of ITAR regulations to this code then you + should confirm it for yourself (and maybe let me know if you come + up with a different answer to the one above) +*/ + +#define DEBUG_X(a,b) ; + +extern int DEBUGLEVEL; + +#include "../exim.h" +#include "auth-spa.h" +#include + + +#ifndef _BYTEORDER_H +# define _BYTEORDER_H + +# define RW_PCVAL(read,inbuf,outbuf,len) \ + { if (read) { PCVAL (inbuf,0,outbuf,len); } \ + else { PSCVAL(inbuf,0,outbuf,len); } } + +# define RW_PIVAL(read,big_endian,inbuf,outbuf,len) \ + { if (read) { if (big_endian) { RPIVAL(inbuf,0,outbuf,len); } else { PIVAL(inbuf,0,outbuf,len); } } \ + else { if (big_endian) { RPSIVAL(inbuf,0,outbuf,len); } else { PSIVAL(inbuf,0,outbuf,len); } } } + +# define RW_PSVAL(read,big_endian,inbuf,outbuf,len) \ + { if (read) { if (big_endian) { RPSVAL(inbuf,0,outbuf,len); } else { PSVAL(inbuf,0,outbuf,len); } } \ + else { if (big_endian) { RPSSVAL(inbuf,0,outbuf,len); } else { PSSVAL(inbuf,0,outbuf,len); } } } + +# define RW_CVAL(read, inbuf, outbuf, offset) \ + { if (read) { (outbuf) = CVAL (inbuf,offset); } \ + else { SCVAL(inbuf,offset,outbuf); } } + +# define RW_IVAL(read, big_endian, inbuf, outbuf, offset) \ + { if (read) { (outbuf) = ((big_endian) ? RIVAL(inbuf,offset) : IVAL (inbuf,offset)); } \ + else { if (big_endian) { RSIVAL(inbuf,offset,outbuf); } else { SIVAL(inbuf,offset,outbuf); } } } + +# define RW_SVAL(read, big_endian, inbuf, outbuf, offset) \ + { if (read) { (outbuf) = ((big_endian) ? RSVAL(inbuf,offset) : SVAL (inbuf,offset)); } \ + else { if (big_endian) { RSSVAL(inbuf,offset,outbuf); } else { SSVAL(inbuf,offset,outbuf); } } } + +# undef CAREFUL_ALIGNMENT + +/* we know that the 386 can handle misalignment and has the "right" + byteorder */ +# ifdef __i386__ +# define CAREFUL_ALIGNMENT 0 +# endif + +# ifndef CAREFUL_ALIGNMENT +# define CAREFUL_ALIGNMENT 1 +# endif + +# define CVAL(buf,pos) ((US (buf))[pos]) +# define PVAL(buf,pos) ((unsigned)CVAL(buf,pos)) +# define SCVAL(buf,pos,val) (CVAL(buf,pos) = (val)) + + +# if CAREFUL_ALIGNMENT + +# define SVAL(buf,pos) (PVAL(buf,pos)|PVAL(buf,(pos)+1)<<8) +# define IVAL(buf,pos) (SVAL(buf,pos)|SVAL(buf,(pos)+2)<<16) +# define SSVALX(buf,pos,val) (CVAL(buf,pos)=(val)&0xFF,CVAL(buf,pos+1)=(val)>>8) +# define SIVALX(buf,pos,val) (SSVALX(buf,pos,val&0xFFFF),SSVALX(buf,pos+2,val>>16)) +# define SVALS(buf,pos) ((int16x)SVAL(buf,pos)) +# define IVALS(buf,pos) ((int32x)IVAL(buf,pos)) +# define SSVAL(buf,pos,val) SSVALX((buf),(pos),((uint16x)(val))) +# define SIVAL(buf,pos,val) SIVALX((buf),(pos),((uint32x)(val))) +# define SSVALS(buf,pos,val) SSVALX((buf),(pos),((int16x)(val))) +# define SIVALS(buf,pos,val) SIVALX((buf),(pos),((int32x)(val))) + +# else /* CAREFUL_ALIGNMENT */ + +/* this handles things for architectures like the 386 that can handle + alignment errors */ +/* + WARNING: This section is dependent on the length of int16x and int32x + being correct +*/ + +/* get single value from an SMB buffer */ +# define SVAL(buf,pos) (*(uint16x *)(CS (buf) + (pos))) +# define IVAL(buf,pos) (*(uint32x *)(CS (buf) + (pos))) +# define SVALS(buf,pos) (*(int16x *)(CS (buf) + (pos))) +# define IVALS(buf,pos) (*(int32x *)(CS (buf) + (pos))) + +/* store single value in an SMB buffer */ +# define SSVAL(buf,pos,val) SVAL(buf,pos)=((uint16x)(val)) +# define SIVAL(buf,pos,val) IVAL(buf,pos)=((uint32x)(val)) +# define SSVALS(buf,pos,val) SVALS(buf,pos)=((int16x)(val)) +# define SIVALS(buf,pos,val) IVALS(buf,pos)=((int32x)(val)) + +# endif /* CAREFUL_ALIGNMENT */ + +/* macros for reading / writing arrays */ + +# define SMBMACRO(macro,buf,pos,val,len,size) \ +{ for (int l = 0; l < (len); l++) (val)[l] = macro((buf), (pos) + (size)*l); } + +# define SSMBMACRO(macro,buf,pos,val,len,size) \ +{ for (int l = 0; l < (len); l++) macro((buf), (pos) + (size)*l, (val)[l]); } + +/* reads multiple data from an SMB buffer */ +# define PCVAL(buf,pos,val,len) SMBMACRO(CVAL,buf,pos,val,len,1) +# define PSVAL(buf,pos,val,len) SMBMACRO(SVAL,buf,pos,val,len,2) +# define PIVAL(buf,pos,val,len) SMBMACRO(IVAL,buf,pos,val,len,4) +# define PCVALS(buf,pos,val,len) SMBMACRO(CVALS,buf,pos,val,len,1) +# define PSVALS(buf,pos,val,len) SMBMACRO(SVALS,buf,pos,val,len,2) +# define PIVALS(buf,pos,val,len) SMBMACRO(IVALS,buf,pos,val,len,4) + +/* stores multiple data in an SMB buffer */ +# define PSCVAL(buf,pos,val,len) SSMBMACRO(SCVAL,buf,pos,val,len,1) +# define PSSVAL(buf,pos,val,len) SSMBMACRO(SSVAL,buf,pos,val,len,2) +# define PSIVAL(buf,pos,val,len) SSMBMACRO(SIVAL,buf,pos,val,len,4) +# define PSCVALS(buf,pos,val,len) SSMBMACRO(SCVALS,buf,pos,val,len,1) +# define PSSVALS(buf,pos,val,len) SSMBMACRO(SSVALS,buf,pos,val,len,2) +# define PSIVALS(buf,pos,val,len) SSMBMACRO(SIVALS,buf,pos,val,len,4) + + +/* now the reverse routines - these are used in nmb packets (mostly) */ +# define SREV(x) ((((x)&0xFF)<<8) | (((x)>>8)&0xFF)) +# define IREV(x) ((SREV(x)<<16) | (SREV((x)>>16))) + +# define RSVAL(buf,pos) SREV(SVAL(buf,pos)) +# define RSVALS(buf,pos) SREV(SVALS(buf,pos)) +# define RIVAL(buf,pos) IREV(IVAL(buf,pos)) +# define RIVALS(buf,pos) IREV(IVALS(buf,pos)) +# define RSSVAL(buf,pos,val) SSVAL(buf,pos,SREV(val)) +# define RSSVALS(buf,pos,val) SSVALS(buf,pos,SREV(val)) +# define RSIVAL(buf,pos,val) SIVAL(buf,pos,IREV(val)) +# define RSIVALS(buf,pos,val) SIVALS(buf,pos,IREV(val)) + +/* reads multiple data from an SMB buffer (big-endian) */ +# define RPSVAL(buf,pos,val,len) SMBMACRO(RSVAL,buf,pos,val,len,2) +# define RPIVAL(buf,pos,val,len) SMBMACRO(RIVAL,buf,pos,val,len,4) +# define RPSVALS(buf,pos,val,len) SMBMACRO(RSVALS,buf,pos,val,len,2) +# define RPIVALS(buf,pos,val,len) SMBMACRO(RIVALS,buf,pos,val,len,4) + +/* stores multiple data in an SMB buffer (big-endian) */ +# define RPSSVAL(buf,pos,val,len) SSMBMACRO(RSSVAL,buf,pos,val,len,2) +# define RPSIVAL(buf,pos,val,len) SSMBMACRO(RSIVAL,buf,pos,val,len,4) +# define RPSSVALS(buf,pos,val,len) SSMBMACRO(RSSVALS,buf,pos,val,len,2) +# define RPSIVALS(buf,pos,val,len) SSMBMACRO(RSIVALS,buf,pos,val,len,4) + +# define DBG_RW_PCVAL(charmode,string,depth,base,read,inbuf,outbuf,len) \ + { RW_PCVAL(read,inbuf,outbuf,len) \ + DEBUG_X(5,("%s%04x %s: ", \ + tab_depth(depth), base,string)); \ + if (charmode) print_asc(5, US (outbuf), (len)); else \ + for (int idx = 0; idx < len; idx++) { DEBUG_X(5,("%02x ", (outbuf)[idx])); } \ + DEBUG_X(5,("\n")); } + +# define DBG_RW_PSVAL(charmode,string,depth,base,read,big_endian,inbuf,outbuf,len) \ + { RW_PSVAL(read,big_endian,inbuf,outbuf,len) \ + DEBUG_X(5,("%s%04x %s: ", \ + tab_depth(depth), base,string)); \ + if (charmode) print_asc(5, US (outbuf), 2*(len)); else \ + for (int idx = 0; idx < len; idx++) { DEBUG_X(5,("%04x ", (outbuf)[idx])); } \ + DEBUG_X(5,("\n")); } + +# define DBG_RW_PIVAL(charmode,string,depth,base,read,big_endian,inbuf,outbuf,len) \ + { RW_PIVAL(read,big_endian,inbuf,outbuf,len) \ + DEBUG_X(5,("%s%04x %s: ", \ + tab_depth(depth), base,string)); \ + if (charmode) print_asc(5, US (outbuf), 4*(len)); else \ + for (int idx = 0; idx < len; idx++) { DEBUG_X(5,("%08x ", (outbuf)[idx])); } \ + DEBUG_X(5,("\n")); } + +# define DBG_RW_CVAL(string,depth,base,read,inbuf,outbuf) \ + { RW_CVAL(read,inbuf,outbuf,0) \ + DEBUG_X(5,("%s%04x %s: %02x\n", \ + tab_depth(depth), base, string, outbuf)); } + +# define DBG_RW_SVAL(string,depth,base,read,big_endian,inbuf,outbuf) \ + { RW_SVAL(read,big_endian,inbuf,outbuf,0) \ + DEBUG_X(5,("%s%04x %s: %04x\n", \ + tab_depth(depth), base, string, outbuf)); } + +# define DBG_RW_IVAL(string,depth,base,read,big_endian,inbuf,outbuf) \ + { RW_IVAL(read,big_endian,inbuf,outbuf,0) \ + DEBUG_X(5,("%s%04x %s: %08x\n", \ + tab_depth(depth), base, string, outbuf)); } + +#endif /* _BYTEORDER_H */ + +void E_P16 (uschar *p14, uschar *p16); +void E_P24 (uschar *p21, uschar *c8, uschar *p24); +void D_P16 (uschar *p14, uschar *in, uschar *out); +void SMBOWFencrypt (uschar passwd[16], uschar * c8, uschar p24[24]); + +void mdfour (uschar *out, uschar *in, int n); + + +/* + * base64.c -- base-64 conversion routines. + * + * For license terms, see the file COPYING in this directory. + * + * This base 64 encoding is defined in RFC2045 section 6.8, + * "Base64 Content-Transfer-Encoding", but lines must not be broken in the + * scheme used here. + */ + +static const char base64digits[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +#define BAD (char) -1 +static const char base64val[] = { + BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, + BAD, + BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, + BAD, + BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, 62, BAD, BAD, BAD, + 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, BAD, BAD, BAD, BAD, BAD, BAD, + BAD, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, BAD, BAD, BAD, BAD, BAD, + BAD, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, BAD, BAD, BAD, BAD, BAD +}; +#define DECODE64(c) (isascii(c) ? base64val[c] : BAD) + +void +spa_bits_to_base64 (uschar *out, const uschar *in, int inlen) +/* raw bytes in quasi-big-endian order to base 64 string (NUL-terminated) */ +{ +for (; inlen >= 3; inlen -= 3) + { + *out++ = base64digits[in[0] >> 2]; + *out++ = base64digits[((in[0] << 4) & 0x30) | (in[1] >> 4)]; + *out++ = base64digits[((in[1] << 2) & 0x3c) | (in[2] >> 6)]; + *out++ = base64digits[in[2] & 0x3f]; + in += 3; + } +if (inlen > 0) + { + uschar fragment; + + *out++ = base64digits[in[0] >> 2]; + fragment = (in[0] << 4) & 0x30; + if (inlen > 1) + fragment |= in[1] >> 4; + *out++ = base64digits[fragment]; + *out++ = (inlen < 2) ? '=' : base64digits[(in[1] << 2) & 0x3c]; + *out++ = '='; + } +*out = '\0'; +} + + +/* The outlength parameter was added by PH, December 2004 */ + +int +spa_base64_to_bits (char *out, int outlength, const char *in) +/* base 64 to raw bytes in quasi-big-endian order, returning count of bytes */ +{ +int len = 0; +uschar digit1, digit2, digit3, digit4; + +if (in[0] == '+' && in[1] == ' ') + in += 2; +if (*in == '\r') + return (0); + +do + { + if (len >= outlength) /* Added by PH */ + return -1; /* Added by PH */ + digit1 = in[0]; + if (DECODE64 (digit1) == BAD) + return -1; + digit2 = in[1]; + if (DECODE64 (digit2) == BAD) + return -1; + digit3 = in[2]; + if (digit3 != '=' && DECODE64 (digit3) == BAD) + return -1; + digit4 = in[3]; + if (digit4 != '=' && DECODE64 (digit4) == BAD) + return -1; + in += 4; + *out++ = (DECODE64 (digit1) << 2) | (DECODE64 (digit2) >> 4); + ++len; + if (digit3 != '=') + { + if (len >= outlength) /* Added by PH */ + return -1; /* Added by PH */ + *out++ = + ((DECODE64 (digit2) << 4) & 0xf0) | (DECODE64 (digit3) >> 2); + ++len; + if (digit4 != '=') + { + if (len >= outlength) /* Added by PH */ + return -1; /* Added by PH */ + *out++ = ((DECODE64 (digit3) << 6) & 0xc0) | DECODE64 (digit4); + ++len; + } + } + } +while (*in && *in != '\r' && digit4 != '='); + +return len; +} + + +static uschar perm1[56] = { 57, 49, 41, 33, 25, 17, 9, + 1, 58, 50, 42, 34, 26, 18, + 10, 2, 59, 51, 43, 35, 27, + 19, 11, 3, 60, 52, 44, 36, + 63, 55, 47, 39, 31, 23, 15, + 7, 62, 54, 46, 38, 30, 22, + 14, 6, 61, 53, 45, 37, 29, + 21, 13, 5, 28, 20, 12, 4 +}; + +static uschar perm2[48] = { 14, 17, 11, 24, 1, 5, + 3, 28, 15, 6, 21, 10, + 23, 19, 12, 4, 26, 8, + 16, 7, 27, 20, 13, 2, + 41, 52, 31, 37, 47, 55, + 30, 40, 51, 45, 33, 48, + 44, 49, 39, 56, 34, 53, + 46, 42, 50, 36, 29, 32 +}; + +static uschar perm3[64] = { 58, 50, 42, 34, 26, 18, 10, 2, + 60, 52, 44, 36, 28, 20, 12, 4, + 62, 54, 46, 38, 30, 22, 14, 6, + 64, 56, 48, 40, 32, 24, 16, 8, + 57, 49, 41, 33, 25, 17, 9, 1, + 59, 51, 43, 35, 27, 19, 11, 3, + 61, 53, 45, 37, 29, 21, 13, 5, + 63, 55, 47, 39, 31, 23, 15, 7 +}; + +static uschar perm4[48] = { 32, 1, 2, 3, 4, 5, + 4, 5, 6, 7, 8, 9, + 8, 9, 10, 11, 12, 13, + 12, 13, 14, 15, 16, 17, + 16, 17, 18, 19, 20, 21, + 20, 21, 22, 23, 24, 25, + 24, 25, 26, 27, 28, 29, + 28, 29, 30, 31, 32, 1 +}; + +static uschar perm5[32] = { 16, 7, 20, 21, + 29, 12, 28, 17, + 1, 15, 23, 26, + 5, 18, 31, 10, + 2, 8, 24, 14, + 32, 27, 3, 9, + 19, 13, 30, 6, + 22, 11, 4, 25 +}; + + +static uschar perm6[64] = { 40, 8, 48, 16, 56, 24, 64, 32, + 39, 7, 47, 15, 55, 23, 63, 31, + 38, 6, 46, 14, 54, 22, 62, 30, + 37, 5, 45, 13, 53, 21, 61, 29, + 36, 4, 44, 12, 52, 20, 60, 28, + 35, 3, 43, 11, 51, 19, 59, 27, + 34, 2, 42, 10, 50, 18, 58, 26, + 33, 1, 41, 9, 49, 17, 57, 25 +}; + + +static uschar sc[16] = { 1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1 }; + +static uschar sbox[8][4][16] = { + {{14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7}, + {0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8}, + {4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0}, + {15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13}}, + + {{15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10}, + {3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5}, + {0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15}, + {13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9}}, + + {{10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8}, + {13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1}, + {13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7}, + {1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12}}, + + {{7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15}, + {13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9}, + {10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4}, + {3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14}}, + + {{2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9}, + {14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6}, + {4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14}, + {11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3}}, + + {{12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11}, + {10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8}, + {9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6}, + {4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13}}, + + {{4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1}, + {13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6}, + {1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2}, + {6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12}}, + + {{13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7}, + {1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2}, + {7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8}, + {2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11}} +}; + +static void +permute (char *out, char *in, uschar * p, int n) +{ +for (int i = 0; i < n; i++) + out[i] = in[p[i] - 1]; +} + +static void +lshift (char *d, int count, int n) +{ +char out[64]; +for (int i = 0; i < n; i++) + out[i] = d[(i + count) % n]; +for (int i = 0; i < n; i++) + d[i] = out[i]; +} + +static void +concat (char *out, char *in1, char *in2, int l1, int l2) +{ +while (l1--) + *out++ = *in1++; +while (l2--) + *out++ = *in2++; +} + +static void +xor (char *out, char *in1, char *in2, int n) +{ +for (int i = 0; i < n; i++) + out[i] = in1[i] ^ in2[i]; +} + +static void +dohash (char *out, char *in, char *key, int forw) +{ +int i, j, k; +char pk1[56]; +char c[28]; +char d[28]; +char cd[56]; +char ki[16][48]; +char pd1[64]; +char l[32], r[32]; +char rl[64]; + +permute (pk1, key, perm1, 56); + +for (i = 0; i < 28; i++) + c[i] = pk1[i]; +for (i = 0; i < 28; i++) + d[i] = pk1[i + 28]; + +for (i = 0; i < 16; i++) + { + lshift (c, sc[i], 28); + lshift (d, sc[i], 28); + + concat (cd, c, d, 28, 28); + permute (ki[i], cd, perm2, 48); + } + +permute (pd1, in, perm3, 64); + +for (j = 0; j < 32; j++) + { + l[j] = pd1[j]; + r[j] = pd1[j + 32]; + } + +for (i = 0; i < 16; i++) + { + char er[48]; + char erk[48]; + char b[8][6]; + char cb[32]; + char pcb[32]; + char r2[32]; + + permute (er, r, perm4, 48); + + xor (erk, er, ki[forw ? i : 15 - i], 48); + + for (j = 0; j < 8; j++) + for (k = 0; k < 6; k++) + b[j][k] = erk[j * 6 + k]; + + for (j = 0; j < 8; j++) + { + int m, n; + m = (b[j][0] << 1) | b[j][5]; + + n = (b[j][1] << 3) | (b[j][2] << 2) | (b[j][3] << 1) | b[j][4]; + + for (k = 0; k < 4; k++) + b[j][k] = (sbox[j][m][n] & (1 << (3 - k))) ? 1 : 0; + } + + for (j = 0; j < 8; j++) + for (k = 0; k < 4; k++) + cb[j * 4 + k] = b[j][k]; + permute (pcb, cb, perm5, 32); + + xor (r2, l, pcb, 32); + + for (j = 0; j < 32; j++) + l[j] = r[j]; + + for (j = 0; j < 32; j++) + r[j] = r2[j]; + } + +concat (rl, r, l, 32, 32); + +permute (out, rl, perm6, 64); +} + +static void +str_to_key (uschar *str, uschar *key) +{ +int i; + +key[0] = str[0] >> 1; +key[1] = ((str[0] & 0x01) << 6) | (str[1] >> 2); +key[2] = ((str[1] & 0x03) << 5) | (str[2] >> 3); +key[3] = ((str[2] & 0x07) << 4) | (str[3] >> 4); +key[4] = ((str[3] & 0x0F) << 3) | (str[4] >> 5); +key[5] = ((str[4] & 0x1F) << 2) | (str[5] >> 6); +key[6] = ((str[5] & 0x3F) << 1) | (str[6] >> 7); +key[7] = str[6] & 0x7F; +for (i = 0; i < 8; i++) + key[i] = (key[i] << 1); +} + + +static void +smbhash (uschar *out, uschar *in, uschar *key, int forw) +{ +int i; +char outb[64]; +char inb[64]; +char keyb[64]; +uschar key2[8]; + +str_to_key (key, key2); + +for (i = 0; i < 64; i++) + { + inb[i] = (in[i / 8] & (1 << (7 - (i % 8)))) ? 1 : 0; + keyb[i] = (key2[i / 8] & (1 << (7 - (i % 8)))) ? 1 : 0; + outb[i] = 0; + } + +dohash (outb, inb, keyb, forw); + +for (i = 0; i < 8; i++) + out[i] = 0; + +for (i = 0; i < 64; i++) + if (outb[i]) + out[i / 8] |= (1 << (7 - (i % 8))); +} + +void +E_P16 (uschar *p14, uschar *p16) +{ +uschar sp8[8] = { 0x4b, 0x47, 0x53, 0x21, 0x40, 0x23, 0x24, 0x25 }; +smbhash (p16, sp8, p14, 1); +smbhash (p16 + 8, sp8, p14 + 7, 1); +} + +void +E_P24 (uschar *p21, uschar *c8, uschar *p24) +{ +smbhash (p24, c8, p21, 1); +smbhash (p24 + 8, c8, p21 + 7, 1); +smbhash (p24 + 16, c8, p21 + 14, 1); +} + +void +D_P16 (uschar *p14, uschar *in, uschar *out) +{ +smbhash (out, in, p14, 0); +smbhash (out + 8, in + 8, p14 + 7, 0); +} + +/**************************************************************************** + Like strncpy but always null terminates. Make sure there is room! + The variable n should always be one less than the available size. +****************************************************************************/ + +char * +StrnCpy (char *dest, const char *src, size_t n) +{ +char *d = dest; +if (!dest) + return (NULL); +if (!src) + { + *dest = 0; + return (dest); + } +while (n-- && (*d++ = *src++)); +*d = 0; +return (dest); +} + +size_t +skip_multibyte_char (char c) +{ +/* bogus if to get rid of unused compiler warning */ +if (c) + return 0; +else + return 0; +} + + +/******************************************************************* +safe string copy into a known length string. maxlength does not +include the terminating zero. +********************************************************************/ + +char * +safe_strcpy (char *dest, const char *src, size_t maxlength) +{ +size_t len; + +if (!dest) + { + DEBUG_X (0, ("ERROR: NULL dest in safe_strcpy\n")); + return NULL; + } + +if (!src) + { + *dest = 0; + return dest; + } + +len = strlen (src); + +if (len > maxlength) + { + DEBUG_X (0, ("ERROR: string overflow by %d in safe_strcpy [%.50s]\n", + (int) (len - maxlength), src)); + len = maxlength; + } + +memcpy (dest, src, len); +dest[len] = 0; +return dest; +} + + +void +strupper (char *s) +{ +while (*s) + { + size_t skip = skip_multibyte_char (*s); + if (skip != 0) + s += skip; + else + { + if (islower ((uschar)(*s))) + *s = toupper (*s); + s++; + } + } +} + + +/* + This implements the X/Open SMB password encryption + It takes a password, a 8 byte "crypt key" and puts 24 bytes of + encrypted password into p24 + */ + +void +spa_smb_encrypt (uschar * passwd, uschar * c8, uschar * p24) +{ +uschar p14[15], p21[21]; + +memset (p21, '\0', 21); +memset (p14, '\0', 14); +StrnCpy (CS p14, CS passwd, 14); + +strupper (CS p14); +E_P16 (p14, p21); + +SMBOWFencrypt (p21, c8, p24); + +#ifdef DEBUG_PASSWORD +DEBUG_X (100, ("spa_smb_encrypt: lm#, challenge, response\n")); +dump_data (100, CS p21, 16); +dump_data (100, CS c8, 8); +dump_data (100, CS p24, 24); +#endif +} + +/* Routines for Windows NT MD4 Hash functions. */ +static int +_my_wcslen (int16x * str) +{ +int len = 0; +while (*str++ != 0) + len++; +return len; +} + +/* + * Convert a string into an NT UNICODE string. + * Note that regardless of processor type + * this must be in intel (little-endian) + * format. + */ + +static int +_my_mbstowcs (int16x * dst, uschar * src, int len) +{ +int i; +int16x val; + +for (i = 0; i < len; i++) + { + val = *src; + SSVAL (dst, 0, val); + dst++; + src++; + if (val == 0) + break; + } +return i; +} + +/* + * Creates the MD4 Hash of the users password in NT UNICODE. + */ + +void +E_md4hash (uschar * passwd, uschar * p16) +{ +int len; +int16x wpwd[129]; + +/* Password cannot be longer than 128 characters */ +len = strlen (CS passwd); +if (len > 128) + len = 128; +/* Password must be converted to NT unicode */ +_my_mbstowcs (wpwd, passwd, len); +wpwd[len] = 0; /* Ensure string is null terminated */ +/* Calculate length in bytes */ +len = _my_wcslen (wpwd) * sizeof (int16x); + +mdfour (p16, US wpwd, len); +} + +/* Does both the NT and LM owfs of a user's password */ +void +nt_lm_owf_gen (char *pwd, uschar nt_p16[16], uschar p16[16]) +{ +char passwd[130]; + +memset (passwd, '\0', 130); +safe_strcpy (passwd, pwd, sizeof (passwd) - 1); + +/* Calculate the MD4 hash (NT compatible) of the password */ +memset (nt_p16, '\0', 16); +E_md4hash (US passwd, nt_p16); + +#ifdef DEBUG_PASSWORD +DEBUG_X (100, ("nt_lm_owf_gen: pwd, nt#\n")); +dump_data (120, passwd, strlen (passwd)); +dump_data (100, CS nt_p16, 16); +#endif + +/* Mangle the passwords into Lanman format */ +passwd[14] = '\0'; +strupper (passwd); + +/* Calculate the SMB (lanman) hash functions of the password */ + +memset (p16, '\0', 16); +E_P16 (US passwd, US p16); + +#ifdef DEBUG_PASSWORD +DEBUG_X (100, ("nt_lm_owf_gen: pwd, lm#\n")); +dump_data (120, passwd, strlen (passwd)); +dump_data (100, CS p16, 16); +#endif +/* clear out local copy of user's password (just being paranoid). */ +memset (passwd, '\0', sizeof (passwd)); +} + +/* Does the des encryption from the NT or LM MD4 hash. */ +void +SMBOWFencrypt (uschar passwd[16], uschar * c8, uschar p24[24]) +{ +uschar p21[21]; + +memset (p21, '\0', 21); + +memcpy (p21, passwd, 16); +E_P24 (p21, c8, p24); +} + +/* Does the des encryption from the FIRST 8 BYTES of the NT or LM MD4 hash. */ +void +NTLMSSPOWFencrypt (uschar passwd[8], uschar * ntlmchalresp, uschar p24[24]) +{ +uschar p21[21]; + +memset (p21, '\0', 21); +memcpy (p21, passwd, 8); +memset (p21 + 8, 0xbd, 8); + +E_P24 (p21, ntlmchalresp, p24); +#ifdef DEBUG_PASSWORD +DEBUG_X (100, ("NTLMSSPOWFencrypt: p21, c8, p24\n")); +dump_data (100, CS p21, 21); +dump_data (100, CS ntlmchalresp, 8); +dump_data (100, CS p24, 24); +#endif +} + + +/* Does the NT MD4 hash then des encryption. */ + +void +spa_smb_nt_encrypt (uschar * passwd, uschar * c8, uschar * p24) +{ +uschar p21[21]; + +memset (p21, '\0', 21); + +E_md4hash (passwd, p21); +SMBOWFencrypt (p21, c8, p24); + +#ifdef DEBUG_PASSWORD +DEBUG_X (100, ("spa_smb_nt_encrypt: nt#, challenge, response\n")); +dump_data (100, CS p21, 16); +dump_data (100, CS c8, 8); +dump_data (100, CS p24, 24); +#endif +} + +static uint32x A, B, C, D; + +static uint32x +F (uint32x X, uint32x Y, uint32x Z) +{ +return (X & Y) | ((~X) & Z); +} + +static uint32x +G (uint32x X, uint32x Y, uint32x Z) +{ +return (X & Y) | (X & Z) | (Y & Z); +} + +static uint32x +H (uint32x X, uint32x Y, uint32x Z) +{ +return X ^ Y ^ Z; +} + +static uint32x +lshift_a (uint32x x, int s) +{ +x &= 0xFFFFFFFF; +return ((x << s) & 0xFFFFFFFF) | (x >> (32 - s)); +} + +#define ROUND1(a,b,c,d,k,s) a = lshift_a(a + F(b,c,d) + X[k], s) +#define ROUND2(a,b,c,d,k,s) a = lshift_a(a + G(b,c,d) + X[k] + (uint32x)0x5A827999,s) +#define ROUND3(a,b,c,d,k,s) a = lshift_a(a + H(b,c,d) + X[k] + (uint32x)0x6ED9EBA1,s) + +/* this applies md4 to 64 byte chunks */ +static void +spa_mdfour64 (uint32x * M) +{ +int j; +uint32x AA, BB, CC, DD; +uint32x X[16]; + +for (j = 0; j < 16; j++) + X[j] = M[j]; + +AA = A; +BB = B; +CC = C; +DD = D; + +ROUND1 (A, B, C, D, 0, 3); +ROUND1 (D, A, B, C, 1, 7); +ROUND1 (C, D, A, B, 2, 11); +ROUND1 (B, C, D, A, 3, 19); +ROUND1 (A, B, C, D, 4, 3); +ROUND1 (D, A, B, C, 5, 7); +ROUND1 (C, D, A, B, 6, 11); +ROUND1 (B, C, D, A, 7, 19); +ROUND1 (A, B, C, D, 8, 3); +ROUND1 (D, A, B, C, 9, 7); +ROUND1 (C, D, A, B, 10, 11); +ROUND1 (B, C, D, A, 11, 19); +ROUND1 (A, B, C, D, 12, 3); +ROUND1 (D, A, B, C, 13, 7); +ROUND1 (C, D, A, B, 14, 11); +ROUND1 (B, C, D, A, 15, 19); + +ROUND2 (A, B, C, D, 0, 3); +ROUND2 (D, A, B, C, 4, 5); +ROUND2 (C, D, A, B, 8, 9); +ROUND2 (B, C, D, A, 12, 13); +ROUND2 (A, B, C, D, 1, 3); +ROUND2 (D, A, B, C, 5, 5); +ROUND2 (C, D, A, B, 9, 9); +ROUND2 (B, C, D, A, 13, 13); +ROUND2 (A, B, C, D, 2, 3); +ROUND2 (D, A, B, C, 6, 5); +ROUND2 (C, D, A, B, 10, 9); +ROUND2 (B, C, D, A, 14, 13); +ROUND2 (A, B, C, D, 3, 3); +ROUND2 (D, A, B, C, 7, 5); +ROUND2 (C, D, A, B, 11, 9); +ROUND2 (B, C, D, A, 15, 13); + +ROUND3 (A, B, C, D, 0, 3); +ROUND3 (D, A, B, C, 8, 9); +ROUND3 (C, D, A, B, 4, 11); +ROUND3 (B, C, D, A, 12, 15); +ROUND3 (A, B, C, D, 2, 3); +ROUND3 (D, A, B, C, 10, 9); +ROUND3 (C, D, A, B, 6, 11); +ROUND3 (B, C, D, A, 14, 15); +ROUND3 (A, B, C, D, 1, 3); +ROUND3 (D, A, B, C, 9, 9); +ROUND3 (C, D, A, B, 5, 11); +ROUND3 (B, C, D, A, 13, 15); +ROUND3 (A, B, C, D, 3, 3); +ROUND3 (D, A, B, C, 11, 9); +ROUND3 (C, D, A, B, 7, 11); +ROUND3 (B, C, D, A, 15, 15); + +A += AA; +B += BB; +C += CC; +D += DD; + +A &= 0xFFFFFFFF; +B &= 0xFFFFFFFF; +C &= 0xFFFFFFFF; +D &= 0xFFFFFFFF; + +for (j = 0; j < 16; j++) + X[j] = 0; +} + +static void +copy64 (uint32x * M, uschar *in) +{ +int i; + +for (i = 0; i < 16; i++) + M[i] = (in[i * 4 + 3] << 24) | (in[i * 4 + 2] << 16) | + (in[i * 4 + 1] << 8) | (in[i * 4 + 0] << 0); +} + +static void +copy4 (uschar *out, uint32x x) +{ +out[0] = x & 0xFF; +out[1] = (x >> 8) & 0xFF; +out[2] = (x >> 16) & 0xFF; +out[3] = (x >> 24) & 0xFF; +} + +/* produce a md4 message digest from data of length n bytes */ +void +mdfour (uschar *out, uschar *in, int n) +{ +uschar buf[128]; +uint32x M[16]; +uint32x b = n * 8; +int i; + +A = 0x67452301; +B = 0xefcdab89; +C = 0x98badcfe; +D = 0x10325476; + +while (n > 64) + { + copy64 (M, in); + spa_mdfour64 (M); + in += 64; + n -= 64; + } + +for (i = 0; i < 128; i++) + buf[i] = 0; +memcpy (buf, in, n); +buf[n] = 0x80; + +if (n <= 55) + { + copy4 (buf + 56, b); + copy64 (M, buf); + spa_mdfour64 (M); + } +else + { + copy4 (buf + 120, b); + copy64 (M, buf); + spa_mdfour64 (M); + copy64 (M, buf + 64); + spa_mdfour64 (M); + } + +for (i = 0; i < 128; i++) + buf[i] = 0; +copy64 (M, buf); + +copy4 (out, A); +copy4 (out + 4, B); +copy4 (out + 8, C); +copy4 (out + 12, D); + +A = B = C = D = 0; +} + +char versionString[] = "libntlm version 0.21"; + +/* Utility routines that handle NTLM auth structures. */ + +/* The [IS]VAL macros are to take care of byte order for non-Intel + * Machines -- I think this file is OK, but it hasn't been tested. + * The other files (the ones stolen from Samba) should be OK. + */ + + +/* I am not crazy about these macros -- they seem to have gotten + * a bit complex. A new scheme for handling string/buffer fields + * in the structures probably needs to be designed + */ + +#define spa_bytes_add(ptr, header, buf, count) \ +{ \ +if (buf && (count) != 0) /* we hate -Wint-in-bool-contex */ \ + { \ + SSVAL(&ptr->header.len,0,count); \ + SSVAL(&ptr->header.maxlen,0,count); \ + SIVAL(&ptr->header.offset,0,((ptr->buffer - ((uint8x*)ptr)) + ptr->bufIndex)); \ + memcpy(ptr->buffer+ptr->bufIndex, buf, count); \ + ptr->bufIndex += count; \ + } \ +else \ + { \ + ptr->header.len = \ + ptr->header.maxlen = 0; \ + SIVAL(&ptr->header.offset,0,((ptr->buffer - ((uint8x*)ptr)) + ptr->bufIndex)); \ + } \ +} + +#define spa_string_add(ptr, header, string) \ +{ \ +char *p = string; \ +int len = 0; \ +if (p) len = strlen(p); \ +spa_bytes_add(ptr, header, (US p), len); \ +} + +#define spa_unicode_add_string(ptr, header, string) \ +{ \ +char *p = string; \ +uschar *b = NULL; \ +int len = 0; \ +if (p) \ + { \ + len = strlen(p); \ + b = strToUnicode(p); \ + } \ +spa_bytes_add(ptr, header, b, len*2); \ +} + + +#define GetUnicodeString(structPtr, header) \ +unicodeToString(((char*)structPtr) + IVAL(&structPtr->header.offset,0) , SVAL(&structPtr->header.len,0)/2) +#define GetString(structPtr, header) \ +toString(((CS structPtr) + IVAL(&structPtr->header.offset,0)), SVAL(&structPtr->header.len,0)) + +#ifdef notdef + +#define DumpBuffer(fp, structPtr, header) \ +dumpRaw(fp,(US structPtr)+IVAL(&structPtr->header.offset,0),SVAL(&structPtr->header.len,0)) + + +static void +dumpRaw (FILE * fp, uschar *buf, size_t len) +{ +int i; + +for (i = 0; i < len; ++i) + fprintf (fp, "%02x ", buf[i]); + +fprintf (fp, "\n"); +} + +#endif + +char * +unicodeToString (char *p, size_t len) +{ +int i; +static char buf[1024]; + +assert (len + 1 < sizeof buf); + +for (i = 0; i < len; ++i) + { + buf[i] = *p & 0x7f; + p += 2; + } + +buf[i] = '\0'; +return buf; +} + +static uschar * +strToUnicode (char *p) +{ +static uschar buf[1024]; +size_t l = strlen (p); +int i = 0; + +assert (l * 2 < sizeof buf); + +while (l--) + { + buf[i++] = *p++; + buf[i++] = 0; + } + +return buf; +} + +static uschar * +toString (char *p, size_t len) +{ +static uschar buf[1024]; + +assert (len + 1 < sizeof buf); + +memcpy (buf, p, len); +buf[len] = 0; +return buf; +} + +#ifdef notdef + +void +dumpSmbNtlmAuthRequest (FILE * fp, SPAAuthRequest * request) +{ +fprintf (fp, "NTLM Request:\n"); +fprintf (fp, " Ident = %s\n", request->ident); +fprintf (fp, " mType = %d\n", IVAL (&request->msgType, 0)); +fprintf (fp, " Flags = %08x\n", IVAL (&request->flags, 0)); +fprintf (fp, " User = %s\n", GetString (request, user)); +fprintf (fp, " Domain = %s\n", GetString (request, domain)); +} + +void +dumpSmbNtlmAuthChallenge (FILE * fp, SPAAuthChallenge * challenge) +{ +fprintf (fp, "NTLM Challenge:\n"); +fprintf (fp, " Ident = %s\n", challenge->ident); +fprintf (fp, " mType = %d\n", IVAL (&challenge->msgType, 0)); +fprintf (fp, " Domain = %s\n", GetUnicodeString (challenge, uDomain)); +fprintf (fp, " Flags = %08x\n", IVAL (&challenge->flags, 0)); +fprintf (fp, " Challenge = "); +dumpRaw (fp, challenge->challengeData, 8); +} + +void +dumpSmbNtlmAuthResponse (FILE * fp, SPAAuthResponse * response) +{ +fprintf (fp, "NTLM Response:\n"); +fprintf (fp, " Ident = %s\n", response->ident); +fprintf (fp, " mType = %d\n", IVAL (&response->msgType, 0)); +fprintf (fp, " LmResp = "); +DumpBuffer (fp, response, lmResponse); +fprintf (fp, " NTResp = "); +DumpBuffer (fp, response, ntResponse); +fprintf (fp, " Domain = %s\n", GetUnicodeString (response, uDomain)); +fprintf (fp, " User = %s\n", GetUnicodeString (response, uUser)); +fprintf (fp, " Wks = %s\n", GetUnicodeString (response, uWks)); +fprintf (fp, " sKey = "); +DumpBuffer (fp, response, sessionKey); +fprintf (fp, " Flags = %08x\n", IVAL (&response->flags, 0)); +} +#endif + +void +spa_build_auth_request (SPAAuthRequest * request, char *user, char *domain) +{ +char *u = strdup (user); +char *p = strchr (u, '@'); + +if (p) + { + if (!domain) + domain = p + 1; + *p = '\0'; + } + +request->bufIndex = 0; +memcpy (request->ident, "NTLMSSP\0\0\0", 8); +SIVAL (&request->msgType, 0, 1); +SIVAL (&request->flags, 0, 0x0000b207); /* have to figure out what these mean */ +spa_string_add (request, user, u); +spa_string_add (request, domain, domain); +free (u); +} + + + +void +spa_build_auth_challenge (SPAAuthRequest * request, SPAAuthChallenge * challenge) +{ +char chalstr[8]; +int i; +int p = (int)getpid(); +int random_seed = (int)time(NULL) ^ ((p << 16) | p); + +/* Ensure challenge data is cleared, in case it isn't all used. This +patch added by PH on suggestion of Russell King */ + +memset(challenge, 0, sizeof(SPAAuthChallenge)); + +challenge->bufIndex = 0; +memcpy (challenge->ident, "NTLMSSP\0", 8); +SIVAL (&challenge->msgType, 0, 2); +SIVAL (&challenge->flags, 0, 0x00008201); +SIVAL (&challenge->uDomain.len, 0, 0x0000); +SIVAL (&challenge->uDomain.maxlen, 0, 0x0000); +SIVAL (&challenge->uDomain.offset, 0, 0x00002800); + +/* generate eight pseudo random bytes (method ripped from host.c) */ + +for(i=0;i<8;i++) + { + chalstr[i] = (uschar)(random_seed >> 16) % 256; + random_seed = (1103515245 - (chalstr[i])) * random_seed + 12345; + } + +memcpy(challenge->challengeData,chalstr,8); +} + + + + +/* This is the original source of this function, preserved here for reference. +The new version below was re-organized by PH following a patch and some further +suggestions from Mark Lyda to fix the problem that is described at the head of +this module. At the same time, I removed the untidiness in the code below that +involves the "d" and "domain" variables. */ + +#ifdef NEVER +void +spa_build_auth_response (SPAAuthChallenge * challenge, + SPAAuthResponse * response, char *user, + char *password) +{ +uint8x lmRespData[24]; +uint8x ntRespData[24]; +char *d = strdup (GetUnicodeString (challenge, uDomain)); +char *domain = d; +char *u = strdup (user); +char *p = strchr (u, '@'); + +if (p) + { + domain = p + 1; + *p = '\0'; + } + +spa_smb_encrypt (US password, challenge->challengeData, lmRespData); +spa_smb_nt_encrypt (US password, challenge->challengeData, ntRespData); + +response->bufIndex = 0; +memcpy (response->ident, "NTLMSSP\0\0\0", 8); +SIVAL (&response->msgType, 0, 3); + +spa_bytes_add (response, lmResponse, lmRespData, 24); +spa_bytes_add (response, ntResponse, ntRespData, 24); +spa_unicode_add_string (response, uDomain, domain); +spa_unicode_add_string (response, uUser, u); +spa_unicode_add_string (response, uWks, u); +spa_string_add (response, sessionKey, NULL); + +response->flags = challenge->flags; + +free (d); +free (u); +} +#endif + + +/* This is the re-organized version (see comments above) */ + +void +spa_build_auth_response (SPAAuthChallenge * challenge, + SPAAuthResponse * response, char *user, + char *password) +{ +uint8x lmRespData[24]; +uint8x ntRespData[24]; +uint32x cf = IVAL(&challenge->flags, 0); +char *u = strdup (user); +char *p = strchr (u, '@'); +char *d = NULL; +char *domain; + +if (p) + { + domain = p + 1; + *p = '\0'; + } + +else domain = d = strdup((cf & 0x1)? + CCS GetUnicodeString(challenge, uDomain) : + CCS GetString(challenge, uDomain)); + +spa_smb_encrypt (US password, challenge->challengeData, lmRespData); +spa_smb_nt_encrypt (US password, challenge->challengeData, ntRespData); + +response->bufIndex = 0; +memcpy (response->ident, "NTLMSSP\0\0\0", 8); +SIVAL (&response->msgType, 0, 3); + +spa_bytes_add (response, lmResponse, lmRespData, (cf & 0x200) ? 24 : 0); +spa_bytes_add (response, ntResponse, ntRespData, (cf & 0x8000) ? 24 : 0); + +if (cf & 0x1) { /* Unicode Text */ + spa_unicode_add_string (response, uDomain, domain); + spa_unicode_add_string (response, uUser, u); + spa_unicode_add_string (response, uWks, u); +} else { /* OEM Text */ + spa_string_add (response, uDomain, domain); + spa_string_add (response, uUser, u); + spa_string_add (response, uWks, u); +} + +spa_string_add (response, sessionKey, NULL); +response->flags = challenge->flags; + +if (d != NULL) free (d); +free (u); +} diff --git a/src/auths/auth-spa.h b/src/auths/auth-spa.h new file mode 100644 index 0000000..cfe1b08 --- /dev/null +++ b/src/auths/auth-spa.h @@ -0,0 +1,92 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* + * This file provides the necessary methods for authenticating with + * Microsoft's Secure Password Authentication. + + * All the code used here was torn by Marc Prud'hommeaux out of the + * Samba project (by Andrew Tridgell, Jeremy Allison, and others). + */ + +/* December 2004: The spa_base64_to_bits() function has no length checking in +it. I have added a check. PH */ + +/* It seems that some systems have existing but different definitions of some +of the following types. I received a complaint about "int16" causing +compilation problems. So I (PH) have renamed them all, to be on the safe side. + +typedef signed short int16; +typedef unsigned short uint16; +typedef unsigned uint32; +typedef unsigned char uint8; +*/ + +typedef signed short int16x; +typedef unsigned short uint16x; +typedef unsigned uint32x; +typedef unsigned char uint8x; + +typedef struct +{ + uint16x len; + uint16x maxlen; + uint32x offset; +} SPAStrHeader; + +typedef struct +{ + char ident[8]; + uint32x msgType; + SPAStrHeader uDomain; + uint32x flags; + uint8x challengeData[8]; + uint8x reserved[8]; + SPAStrHeader emptyString; + uint8x buffer[1024]; + uint32x bufIndex; +} SPAAuthChallenge; + + +typedef struct +{ + char ident[8]; + uint32x msgType; + uint32x flags; + SPAStrHeader user; + SPAStrHeader domain; + uint8x buffer[1024]; + uint32x bufIndex; +} SPAAuthRequest; + +typedef struct +{ + char ident[8]; + uint32x msgType; + SPAStrHeader lmResponse; + SPAStrHeader ntResponse; + SPAStrHeader uDomain; + SPAStrHeader uUser; + SPAStrHeader uWks; + SPAStrHeader sessionKey; + uint32x flags; + uint8x buffer[1024]; + uint32x bufIndex; +} SPAAuthResponse; + +#define spa_request_length(ptr) (((ptr)->buffer - (uint8x*)(ptr)) + (ptr)->bufIndex) + +void spa_bits_to_base64 (unsigned char *, const unsigned char *, int); +int spa_base64_to_bits(char *, int, const char *); +void spa_build_auth_response (SPAAuthChallenge *challenge, + SPAAuthResponse *response, char *user, char *password); +void spa_build_auth_request (SPAAuthRequest *request, char *user, + char *domain); +extern void spa_smb_encrypt (unsigned char * passwd, unsigned char * c8, + unsigned char * p24); +extern void spa_smb_nt_encrypt (unsigned char * passwd, unsigned char * c8, + unsigned char * p24); +extern char *unicodeToString(char *p, size_t len); +extern void spa_build_auth_challenge(SPAAuthRequest *, SPAAuthChallenge *); + diff --git a/src/auths/call_pam.c b/src/auths/call_pam.c new file mode 100644 index 0000000..80f80f1 --- /dev/null +++ b/src/auths/call_pam.c @@ -0,0 +1,204 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* Copyright (c) The Exim Maintainers 2020 - 2021 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" + +/* This module contains functions that call the PAM authentication mechanism +defined by Sun for Solaris and also available for Linux and other OS. + +We can't just compile this code and allow the library mechanism to omit the +functions if they are not wanted, because we need to have the PAM headers +available for compiling. Therefore, compile these functions only if SUPPORT_PAM +is defined. However, some compilers don't like compiling empty modules, so keep +them happy with a dummy when skipping the rest. Make it reference itself to +stop picky compilers complaining that it is unused, and put in a dummy argument +to stop even pickier compilers complaining about infinite loops. +Then use a mutually-recursive pair as gcc is just getting stupid. */ + +#ifndef SUPPORT_PAM +static void dummy(int x); +static void dummy2(int x) { dummy(x-1); } +static void dummy(int x) { dummy2(x-1); } +#else /* SUPPORT_PAM */ + +#ifdef PAM_H_IN_PAM +#include +#else +#include +#endif + +/* According to the specification, it should be possible to have an application +data pointer passed to the conversation function. However, I was unable to get +this to work on Solaris 2.6, so static variables are used instead. */ + +static int pam_conv_had_error; +static const uschar *pam_args; +static BOOL pam_arg_ended; + + + +/************************************************* +* PAM conversation function * +*************************************************/ + +/* This function is passed to the PAM authentication function, and it calls it +back when it wants data from the client. The string list is in pam_args. When +we reach the end, we pass back an empty string once. If this function is called +again, it will give an error response. This is protection against something +crazy happening. + +Arguments: + num_msg number of messages associated with the call + msg points to an array of length num_msg of pam_message structures + resp set to point to the response block, which has to be got by + this function + appdata_ptr the application data pointer - not used because in Solaris + 2.6 it always arrived in pam_converse() as NULL + +Returns: a PAM return code +*/ + +static int +pam_converse (int num_msg, PAM_CONVERSE_ARG2_TYPE **msg, + struct pam_response **resp, void *appdata_ptr) +{ +int sep = 0; +struct pam_response *reply; + +/* It seems that PAM frees reply[] */ + +if ( pam_arg_ended + || !(reply = malloc(sizeof(struct pam_response) * num_msg))) + return PAM_CONV_ERR; + +for (int i = 0; i < num_msg; i++) + { + uschar *arg; + switch (msg[i]->msg_style) + { + case PAM_PROMPT_ECHO_ON: + case PAM_PROMPT_ECHO_OFF: + if (!(arg = string_nextinlist(&pam_args, &sep, NULL, 0))) + { + arg = US""; + pam_arg_ended = TRUE; + } + reply[i].resp = strdup(CCS arg); /* Use libc malloc, PAM frees resp directly*/ + reply[i].resp_retcode = PAM_SUCCESS; + break; + + case PAM_TEXT_INFO: /* Just acknowledge messages */ + case PAM_ERROR_MSG: + reply[i].resp_retcode = PAM_SUCCESS; + reply[i].resp = NULL; + break; + + default: /* Must be an error of some sort... */ + free(reply); + pam_conv_had_error = TRUE; + return PAM_CONV_ERR; + } + } + +*resp = reply; +return PAM_SUCCESS; +} + + + +/************************************************* +* Perform PAM authentication * +*************************************************/ + +/* This function calls the PAM authentication mechanism, passing over one or +more data strings. + +Arguments: + s a colon-separated list of strings + errptr where to point an error message + +Returns: OK if authentication succeeded + FAIL if authentication failed + ERROR some other error condition +*/ + +int +auth_call_pam(const uschar *s, uschar **errptr) +{ +pam_handle_t *pamh = NULL; +struct pam_conv pamc; +int pam_error; +int sep = 0; +uschar *user; + +/* Set up the input data structure: the address of the conversation function, +and a pointer to application data, which we don't use because I couldn't get it +to work under Solaris 2.6 - it always arrived in pam_converse() as NULL. */ + +pamc.conv = pam_converse; +pamc.appdata_ptr = NULL; + +/* Initialize the static data - the current input data, the error flag, and the +flag for data end. */ + +pam_args = s; +pam_conv_had_error = FALSE; +pam_arg_ended = FALSE; + +/* The first string in the list is the user. If this is an empty string, we +fail. PAM doesn't support authentication with an empty user (it prompts for it, +causing a potential mis-interpretation). */ + +user = string_nextinlist(&pam_args, &sep, NULL, 0); +if (user == NULL || user[0] == 0) return FAIL; + +/* Start off PAM interaction */ + +DEBUG(D_auth) + debug_printf("Running PAM authentication for user \"%s\"\n", user); + +pam_error = pam_start ("exim", CS user, &pamc, &pamh); + +/* Do the authentication - the pam_authenticate() will call pam_converse() to +get the data it wants. After successful authentication we call pam_acct_mgmt() +to apply any other restrictions (e.g. only some times of day). */ + +if (pam_error == PAM_SUCCESS) + { + pam_error = pam_authenticate (pamh, PAM_SILENT); + if (pam_error == PAM_SUCCESS && !pam_conv_had_error) + pam_error = pam_acct_mgmt (pamh, PAM_SILENT); + } + +/* Finish the PAM interaction - this causes it to clean up store etc. Unclear +what should be passed as the second argument. */ + +pam_end(pamh, PAM_SUCCESS); + +/* Sort out the return code. If not success, set the error message. */ + +if (pam_error == PAM_SUCCESS) + { + DEBUG(D_auth) debug_printf("PAM success\n"); + return OK; + } + +*errptr = US pam_strerror(pamh, pam_error); +DEBUG(D_auth) debug_printf("PAM error: %s\n", *errptr); + +if (pam_error == PAM_USER_UNKNOWN || + pam_error == PAM_AUTH_ERR || + pam_error == PAM_ACCT_EXPIRED) + return FAIL; + +return ERROR; +} + +#endif /* SUPPORT_PAM */ + +/* End of call_pam.c */ diff --git a/src/auths/call_pwcheck.c b/src/auths/call_pwcheck.c new file mode 100644 index 0000000..0adde44 --- /dev/null +++ b/src/auths/call_pwcheck.c @@ -0,0 +1,121 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2015 */ +/* Copyright (c) The Exim Maintainers 2020 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* This module contains interface functions to the two Cyrus authentication +daemons. The original one was "pwcheck", which gives its name to the source +file. This is now deprecated in favour of "saslauthd". */ + + +#include "../exim.h" +#include "pwcheck.h" + + +/************************************************* +* External entry point for pwcheck * +*************************************************/ + +/* This function calls the now-deprecated "pwcheck" Cyrus-SASL authentication +daemon, passing over a colon-separated user name and password. As this is +called from the string expander, the string will always be in dynamic store and +can be overwritten. + +Arguments: + s a colon-separated username:password string + errptr where to point an error message + +Returns: OK if authentication succeeded + FAIL if authentication failed + ERROR some other error condition +*/ + +int +auth_call_pwcheck(uschar *s, uschar **errptr) +{ +uschar *reply = NULL; +uschar *pw = Ustrrchr(s, ':'); + +if (pw == NULL) + { + *errptr = US"pwcheck: malformed input - missing colon"; + return ERROR; + } + +*pw++ = 0; /* Separate user and password */ + +DEBUG(D_auth) + debug_printf("Running pwcheck authentication for user \"%s\"\n", s); + +switch (pwcheck_verify_password(CS s, CS pw, CCSS &reply)) + { + case PWCHECK_OK: + DEBUG(D_auth) debug_printf("pwcheck: success (%s)\n", reply); + return OK; + + case PWCHECK_NO: + DEBUG(D_auth) debug_printf("pwcheck: access denied (%s)\n", reply); + return FAIL; + + default: + DEBUG(D_auth) debug_printf("pwcheck: query failed (%s)\n", reply); + *errptr = reply; + return ERROR; + } +} + + +/************************************************* +* External entry point for pwauthd * +*************************************************/ + +/* This function calls the "saslauthd" Cyrus-SASL authentication daemon, +saslauthd, As this is called from the string expander, all the strings will +always be in dynamic store and can be overwritten. + +Arguments: + username username + password password + service optional service + realm optional realm + errptr where to point an error message + +Returns: OK if authentication succeeded + FAIL if authentication failed + ERROR some other error condition +*/ + +int +auth_call_saslauthd(const uschar *username, const uschar *password, + const uschar *service, const uschar *realm, uschar **errptr) +{ +uschar *reply = NULL; + +if (service == NULL) service = US""; +if (realm == NULL) realm = US""; + +DEBUG(D_auth) + debug_printf("Running saslauthd authentication for user \"%s\" \n", username); + +switch (saslauthd_verify_password(username, password, service, + realm, (const uschar **)(&reply))) + { + case PWCHECK_OK: + DEBUG(D_auth) debug_printf("saslauthd: success (%s)\n", reply); + return OK; + + case PWCHECK_NO: + DEBUG(D_auth) debug_printf("saslauthd: access denied (%s)\n", reply); + return FAIL; + + default: + DEBUG(D_auth) debug_printf("saslauthd: query failed (%s)\n", reply); + *errptr = reply; + return ERROR; + } +} + +/* End of call_pwcheck.c */ diff --git a/src/auths/call_radius.c b/src/auths/call_radius.c new file mode 100644 index 0000000..e7f9f52 --- /dev/null +++ b/src/auths/call_radius.c @@ -0,0 +1,223 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* Copyright (c) University of Cambridge 1995 - 2016 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* This file was originally supplied by Ian Kirk. The libradius support came +from Alex Kiernan. */ + +#include "../exim.h" + +/* This module contains functions that call the Radius authentication +mechanism. + +We can't just compile this code and allow the library mechanism to omit the +functions if they are not wanted, because we need to have the Radius headers +available for compiling. Therefore, compile these functions only if +RADIUS_CONFIG_FILE is defined. However, some compilers don't like compiling +empty modules, so keep them happy with a dummy when skipping the rest. Make it +reference itself to stop picky compilers complaining that it is unused, and put +in a dummy argument to stop even pickier compilers complaining about infinite +loops. Then use a mutually-recursive pair as gcc is just getting stupid. */ + +#ifndef RADIUS_CONFIG_FILE +static void dummy(int x); +static void dummy2(int x) { dummy(x-1); } +static void dummy(int x) { dummy2(x-1); } +#else /* RADIUS_CONFIG_FILE */ + + +/* Two different Radius libraries are supported. The default is radiusclient, +using its original API. At release 0.4.0 the API changed. */ + +#ifdef RADIUS_LIB_RADLIB +# include +#else +# if !defined(RADIUS_LIB_RADIUSCLIENT) && !defined(RADIUS_LIB_RADIUSCLIENTNEW) +# define RADIUS_LIB_RADIUSCLIENT +# endif + +# ifdef RADIUS_LIB_RADIUSCLIENTNEW +# define ENV FREERADIUSCLIENT_ENV /* Avoid clash with Berkeley DB */ +# include +# else +# include +# endif +#endif + + + +/************************************************* +* Perform RADIUS authentication * +*************************************************/ + +/* This function calls the Radius authentication mechanism, passing over one or +more data strings. + +Arguments: + s a colon-separated list of strings + errptr where to point an error message + +Returns: OK if authentication succeeded + FAIL if authentication failed + ERROR some other error condition +*/ + +int +auth_call_radius(const uschar *s, uschar **errptr) +{ +uschar *user; +const uschar *radius_args = s; +int result; +int sep = 0; + +#ifdef RADIUS_LIB_RADLIB + struct rad_handle *h; +#else + #ifdef RADIUS_LIB_RADIUSCLIENTNEW + rc_handle *h; + #endif + VALUE_PAIR *send = NULL; + VALUE_PAIR *received; + unsigned int service = PW_AUTHENTICATE_ONLY; + char msg[4096]; +#endif + + +if (!(user = string_nextinlist(&radius_args, &sep, NULL, 0))) user = US""; + +DEBUG(D_auth) debug_printf("Running RADIUS authentication for user \"%s\" " + "and \"%s\"\n", user, radius_args); + +*errptr = NULL; + + +/* Authenticate using the radiusclient library */ + +#ifndef RADIUS_LIB_RADLIB + +rc_openlog("exim"); + +#ifdef RADIUS_LIB_RADIUSCLIENT +if (rc_read_config(RADIUS_CONFIG_FILE) != 0) + *errptr = string_sprintf("RADIUS: can't open %s", RADIUS_CONFIG_FILE); + +else if (rc_read_dictionary(rc_conf_str("dictionary")) != 0) + *errptr = US"RADIUS: can't read dictionary"; + +else if (!rc_avpair_add(&send, PW_USER_NAME, user, 0)) + *errptr = US"RADIUS: add user name failed"; + +else if (!rc_avpair_add(&send, PW_USER_PASSWORD, CS radius_args, 0)) + *errptr = US"RADIUS: add password failed"); + +else if (!rc_avpair_add(&send, PW_SERVICE_TYPE, &service, 0)) + *errptr = US"RADIUS: add service type failed"; + +#else /* RADIUS_LIB_RADIUSCLIENT unset => RADIUS_LIB_RADIUSCLIENT2 */ + +if (!(h = rc_read_config(RADIUS_CONFIG_FILE))) + *errptr = string_sprintf("RADIUS: can't open %s", RADIUS_CONFIG_FILE); + +else if (rc_read_dictionary(h, rc_conf_str(h, "dictionary")) != 0) + *errptr = US"RADIUS: can't read dictionary"; + +else if (!rc_avpair_add(h, &send, PW_USER_NAME, user, Ustrlen(user), 0)) + *errptr = US"RADIUS: add user name failed"; + +else if (!rc_avpair_add(h, &send, PW_USER_PASSWORD, CS radius_args, + Ustrlen(radius_args), 0)) + *errptr = US"RADIUS: add password failed"; + +else if (!rc_avpair_add(h, &send, PW_SERVICE_TYPE, &service, 0, 0)) + *errptr = US"RADIUS: add service type failed"; + +#endif /* RADIUS_LIB_RADIUSCLIENT */ + +if (*errptr) + { + DEBUG(D_auth) debug_printf("%s\n", *errptr); + return ERROR; + } + +#ifdef RADIUS_LIB_RADIUSCLIENT +result = rc_auth(0, send, &received, msg); +#else +result = rc_auth(h, 0, send, &received, msg); +#endif + +DEBUG(D_auth) debug_printf("RADIUS code returned %d\n", result); + +switch (result) + { + case OK_RC: + return OK; + + case REJECT_RC: + case ERROR_RC: + return FAIL; + + case TIMEOUT_RC: + *errptr = US"RADIUS: timed out"; + return ERROR; + + case BADRESP_RC: + default: + *errptr = string_sprintf("RADIUS: unexpected response (%d)", result); + return ERROR; + } + +#else /* RADIUS_LIB_RADLIB is set */ + +/* Authenticate using the libradius library */ + +if (!(h = rad_auth_open())) + { + *errptr = string_sprintf("RADIUS: can't initialise libradius"); + return ERROR; + } +if (rad_config(h, RADIUS_CONFIG_FILE) != 0 || + rad_create_request(h, RAD_ACCESS_REQUEST) != 0 || + rad_put_string(h, RAD_USER_NAME, CS user) != 0 || + rad_put_string(h, RAD_USER_PASSWORD, CS radius_args) != 0 || + rad_put_int(h, RAD_SERVICE_TYPE, RAD_AUTHENTICATE_ONLY) != 0 || + rad_put_string(h, RAD_NAS_IDENTIFIER, CS primary_hostname) != 0) + { + *errptr = string_sprintf("RADIUS: %s", rad_strerror(h)); + result = ERROR; + } +else + switch (result = rad_send_request(h)) + { + case RAD_ACCESS_ACCEPT: + result = OK; + break; + + case RAD_ACCESS_REJECT: + result = FAIL; + break; + + case -1: + *errptr = string_sprintf("RADIUS: %s", rad_strerror(h)); + result = ERROR; + break; + + default: + *errptr = string_sprintf("RADIUS: unexpected response (%d)", result); + result= ERROR; + break; + } + +if (*errptr) DEBUG(D_auth) debug_printf("%s\n", *errptr); +rad_close(h); +return result; + +#endif /* RADIUS_LIB_RADLIB */ +} + +#endif /* RADIUS_CONFIG_FILE */ + +/* End of call_radius.c */ diff --git a/src/auths/check_serv_cond.c b/src/auths/check_serv_cond.c new file mode 100644 index 0000000..457a715 --- /dev/null +++ b/src/auths/check_serv_cond.c @@ -0,0 +1,124 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2012 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" + +/* This module contains the function server_condition(), which is used +by all authenticators. */ + + +/************************************************* +* Check server_condition * +*************************************************/ + +/* This function is called from the server code of all authenticators. For +plaintext and gsasl, it is always called: the argument cannot be empty, because +for those, setting server_condition is what enables it as a server +authenticator. For all the other authenticators, this function is called after +they have authenticated, to enable additional authorization to be done. + +Argument: the authenticator's instance block + +Returns: + OK NULL argument, or success + DEFER couldn't complete the check + FAIL authentication failed +*/ + +int +auth_check_serv_cond(auth_instance *ablock) +{ + return auth_check_some_cond(ablock, + US"server_condition", ablock->server_condition, OK); +} + + +/************************************************* +* Check some server condition * +*************************************************/ + +/* This underlies server_condition, but is also used for some more generic + checks. + +Arguments: + ablock the authenticator's instance block + label debugging label naming the string checked + condition the condition string to be expanded and checked + unset value to return on NULL condition + +Returns: + OK success (or unset=OK) + DEFER couldn't complete the check + FAIL authentication failed +*/ + +int +auth_check_some_cond(auth_instance *ablock, + uschar *label, uschar *condition, int unset) +{ +uschar *cond; + +HDEBUG(D_auth) + { + debug_printf("%s authenticator %s:\n", ablock->name, label); + for (int i = 0; i < AUTH_VARS; i++) if (auth_vars[i]) + debug_printf(" $auth%d = %s\n", i + 1, auth_vars[i]); + for (int i = 1; i <= expand_nmax; i++) + debug_printf(" $%d = %.*s\n", i, expand_nlength[i], expand_nstring[i]); + debug_print_string(ablock->server_debug_string); /* customized debug */ + } + +/* For the plaintext authenticator, server_condition is never NULL. For the +rest, an unset condition lets everything through. */ + +/* For server_condition, an unset condition lets everything through. +For plaintext/gsasl authenticators, it will have been pre-checked to prevent +this. We return the unset scenario value given to us, which for +server_condition will be OK and otherwise will typically be FAIL. */ + +if (!condition) return unset; +cond = expand_string(condition); + +HDEBUG(D_auth) + if (!cond) + debug_printf("expansion failed: %s\n", expand_string_message); + else + debug_printf("expanded string: %s\n", cond); + +/* A forced expansion failure causes authentication to fail. Other expansion +failures yield DEFER, which will cause a temporary error code to be returned to +the AUTH command. The problem is at the server end, so the client should try +again later. */ + +if (!cond) + { + if (f.expand_string_forcedfail) return FAIL; + auth_defer_msg = expand_string_message; + return DEFER; + } + +/* Return FAIL for empty string, "0", "no", and "false"; return OK for +"1", "yes", and "true"; return DEFER for anything else, with the string +available as an error text for the user. */ + +if (*cond == 0 || + Ustrcmp(cond, "0") == 0 || + strcmpic(cond, US"no") == 0 || + strcmpic(cond, US"false") == 0) + return FAIL; + +if (Ustrcmp(cond, "1") == 0 || + strcmpic(cond, US"yes") == 0 || + strcmpic(cond, US"true") == 0) + return OK; + +auth_defer_msg = cond; +auth_defer_user_msg = string_sprintf(": %s", cond); +return DEFER; +} + +/* End of check_serv_cond.c */ diff --git a/src/auths/cram_md5.c b/src/auths/cram_md5.c new file mode 100644 index 0000000..2c0616c --- /dev/null +++ b/src/auths/cram_md5.c @@ -0,0 +1,360 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* Copyright (c) The Exim Maintainers 2020 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +/* The stand-alone version just tests the algorithm. We have to drag +in the MD5 computation functions, without their own stand-alone main +program. */ + +#ifdef STAND_ALONE +#define CRAM_STAND_ALONE +#include "md5.c" + + +/* This is the normal, non-stand-alone case */ + +#else +#include "../exim.h" +#include "cram_md5.h" + +/* Options specific to the cram_md5 authentication mechanism. */ + +optionlist auth_cram_md5_options[] = { + { "client_name", opt_stringptr, + OPT_OFF(auth_cram_md5_options_block, client_name) }, + { "client_secret", opt_stringptr, + OPT_OFF(auth_cram_md5_options_block, client_secret) }, + { "server_secret", opt_stringptr, + OPT_OFF(auth_cram_md5_options_block, server_secret) } +}; + +/* Size of the options list. An extern variable has to be used so that its +address can appear in the tables drtables.c. */ + +int auth_cram_md5_options_count = + sizeof(auth_cram_md5_options)/sizeof(optionlist); + +/* Default private options block for the condition authentication method. */ + +auth_cram_md5_options_block auth_cram_md5_option_defaults = { + NULL, /* server_secret */ + NULL, /* client_secret */ + NULL /* client_name */ +}; + + +#ifdef MACRO_PREDEF + +/* Dummy values */ +void auth_cram_md5_init(auth_instance *ablock) {} +int auth_cram_md5_server(auth_instance *ablock, uschar *data) {return 0;} +int auth_cram_md5_client(auth_instance *ablock, void *sx, int timeout, + uschar *buffer, int buffsize) {return 0;} + +#else /*!MACRO_PREDEF*/ + + +/************************************************* +* Initialization entry point * +*************************************************/ + +/* Called for each instance, after its options have been read, to +enable consistency checks to be done, or anything else that needs +to be set up. */ + +void +auth_cram_md5_init(auth_instance *ablock) +{ +auth_cram_md5_options_block *ob = + (auth_cram_md5_options_block *)(ablock->options_block); +if (ob->server_secret != NULL) ablock->server = TRUE; +if (ob->client_secret != NULL) + { + ablock->client = TRUE; + if (ob->client_name == NULL) ob->client_name = primary_hostname; + } +} + +#endif /*!MACRO_PREDEF*/ +#endif /* STAND_ALONE */ + + + +#ifndef MACRO_PREDEF +/************************************************* +* Perform the CRAM-MD5 algorithm * +*************************************************/ + +/* The CRAM-MD5 algorithm is described in RFC 2195. It computes + + MD5((secret XOR opad), MD5((secret XOR ipad), challenge)) + +where secret is padded out to 64 characters (after being reduced to an MD5 +digest if longer than 64) and ipad and opad are 64-byte strings of 0x36 and +0x5c respectively, and comma means concatenation. + +Arguments: + secret the shared secret + challenge the challenge text + digest 16-byte slot to put the answer in + +Returns: nothing +*/ + +static void +compute_cram_md5(uschar *secret, uschar *challenge, uschar *digestptr) +{ +md5 base; +int len = Ustrlen(secret); +uschar isecret[64]; +uschar osecret[64]; +uschar md5secret[16]; + +/* If the secret is longer than 64 characters, we compute its MD5 digest +and use that. */ + +if (len > 64) + { + md5_start(&base); + md5_end(&base, US secret, len, md5secret); + secret = US md5secret; + len = 16; + } + +/* The key length is now known to be <= 64. Set up the padded and xor'ed +versions. */ + +memcpy(isecret, secret, len); +memset(isecret+len, 0, 64-len); +memcpy(osecret, isecret, 64); + +for (int i = 0; i < 64; i++) + { + isecret[i] ^= 0x36; + osecret[i] ^= 0x5c; + } + +/* Compute the inner MD5 digest */ + +md5_start(&base); +md5_mid(&base, isecret); +md5_end(&base, US challenge, Ustrlen(challenge), md5secret); + +/* Compute the outer MD5 digest */ + +md5_start(&base); +md5_mid(&base, osecret); +md5_end(&base, md5secret, 16, digestptr); +} + + +#ifndef STAND_ALONE + +/************************************************* +* Server entry point * +*************************************************/ + +/* For interface, see auths/README */ + +int +auth_cram_md5_server(auth_instance *ablock, uschar *data) +{ +auth_cram_md5_options_block *ob = + (auth_cram_md5_options_block *)(ablock->options_block); +uschar *challenge = string_sprintf("<%d.%ld@%s>", getpid(), + (long int) time(NULL), primary_hostname); +uschar *clear, *secret; +uschar digest[16]; +int i, rc, len; + +/* If we are running in the test harness, always send the same challenge, +an example string taken from the RFC. */ + +if (f.running_in_test_harness) + challenge = US"<1896.697170952@postoffice.reston.mci.net>"; + +/* No data should have been sent with the AUTH command */ + +if (*data) return UNEXPECTED; + +/* Send the challenge, read the return */ + +if ((rc = auth_get_data(&data, challenge, Ustrlen(challenge))) != OK) return rc; +if ((len = b64decode(data, &clear)) < 0) return BAD64; + +/* The return consists of a user name, space-separated from the CRAM-MD5 +digest, expressed in hex. Extract the user name and put it in $auth1 and $1. +The former is now the preferred variable; the latter is the original one. Then +check that the remaining length is 32. */ + +auth_vars[0] = expand_nstring[1] = clear; +while (*clear && !isspace(*clear)) clear++; +if (!isspace(*clear)) return FAIL; +*clear++ = 0; + +expand_nlength[1] = clear - expand_nstring[1] - 1; +if (len - expand_nlength[1] - 1 != 32) return FAIL; +expand_nmax = 1; + +/* Expand the server_secret string so that it can compute a value dependent on +the user name if necessary. */ + +debug_print_string(ablock->server_debug_string); /* customized debugging */ +secret = expand_string(ob->server_secret); + +/* A forced fail implies failure of authentication - i.e. we have no secret for +the given name. */ + +if (secret == NULL) + { + if (f.expand_string_forcedfail) return FAIL; + auth_defer_msg = expand_string_message; + return DEFER; + } + +/* Compute the CRAM-MD5 digest that we should have received from the client. */ + +compute_cram_md5(secret, challenge, digest); + +HDEBUG(D_auth) + { + uschar buff[64]; + debug_printf("CRAM-MD5: user name = %s\n", auth_vars[0]); + debug_printf(" challenge = %s\n", challenge); + debug_printf(" received = %s\n", clear); + Ustrcpy(buff, US" digest = "); + for (i = 0; i < 16; i++) sprintf(CS buff+22+2*i, "%02x", digest[i]); + debug_printf("%.54s\n", buff); + } + +/* We now have to compare the digest, which is 16 bytes in binary, with the +data received, which is expressed in lower case hex. We checked above that +there were 32 characters of data left. */ + +for (i = 0; i < 16; i++) + { + int a = *clear++; + int b = *clear++; + if (((((a >= 'a')? a - 'a' + 10 : a - '0') << 4) + + ((b >= 'a')? b - 'a' + 10 : b - '0')) != digest[i]) return FAIL; + } + +/* Expand server_condition as an authorization check */ +return auth_check_serv_cond(ablock); +} + + + +/************************************************* +* Client entry point * +*************************************************/ + +/* For interface, see auths/README */ + +int +auth_cram_md5_client( + auth_instance *ablock, /* authenticator block */ + void * sx, /* smtp connextion */ + int timeout, /* command timeout */ + uschar *buffer, /* for reading response */ + int buffsize) /* size of buffer */ +{ +auth_cram_md5_options_block *ob = + (auth_cram_md5_options_block *)(ablock->options_block); +uschar *secret = expand_string(ob->client_secret); +uschar *name = expand_string(ob->client_name); +uschar *challenge, *p; +int i; +uschar digest[16]; + +/* If expansion of either the secret or the user name failed, return CANCELLED +or ERROR, as appropriate. */ + +if (!secret || !name) + { + if (f.expand_string_forcedfail) + { + *buffer = 0; /* No message */ + return CANCELLED; + } + string_format(buffer, buffsize, "expansion of \"%s\" failed in " + "%s authenticator: %s", + !secret ? ob->client_secret : ob->client_name, + ablock->name, expand_string_message); + return ERROR; + } + +/* Initiate the authentication exchange and read the challenge, which arrives +in base 64. */ + +if (smtp_write_command(sx, SCMD_FLUSH, "AUTH %s\r\n", ablock->public_name) < 0) + return FAIL_SEND; +if (!smtp_read_response(sx, buffer, buffsize, '3', timeout)) + return FAIL; + +if (b64decode(buffer + 4, &challenge) < 0) + { + string_format(buffer, buffsize, "bad base 64 string in challenge: %s", + big_buffer + 4); + return ERROR; + } + +/* Run the CRAM-MD5 algorithm on the secret and the challenge */ + +compute_cram_md5(secret, challenge, digest); + +/* Create the response from the user name plus the CRAM-MD5 digest */ + +string_format(big_buffer, big_buffer_size - 36, "%s", name); +for (p = big_buffer; *p; ) p++; +*p++ = ' '; + +for (i = 0; i < 16; i++) + p += sprintf(CS p, "%02x", digest[i]); + +/* Send the response, in base 64, and check the result. The response is +in big_buffer, but b64encode() returns its result in working store, +so calling smtp_write_command(), which uses big_buffer, is OK. */ + +buffer[0] = 0; +if (smtp_write_command(sx, SCMD_FLUSH, "%s\r\n", b64encode(CUS big_buffer, + p - big_buffer)) < 0) return FAIL_SEND; + +return smtp_read_response(sx, US buffer, buffsize, '2', timeout) + ? OK : FAIL; +} +#endif /* STAND_ALONE */ + + +/************************************************* +************************************************** +* Stand-alone test program * +************************************************** +*************************************************/ + +#ifdef STAND_ALONE + +int main(int argc, char **argv) +{ +int i; +uschar *secret = US argv[1]; +uschar *challenge = US argv[2]; +uschar digest[16]; + +compute_cram_md5(secret, challenge, digest); + +for (i = 0; i < 16; i++) printf("%02x", digest[i]); +printf("\n"); + +return 0; +} + +#endif + +#endif /*!MACRO_PREDEF*/ +/* End of cram_md5.c */ diff --git a/src/auths/cram_md5.h b/src/auths/cram_md5.h new file mode 100644 index 0000000..95644db --- /dev/null +++ b/src/auths/cram_md5.h @@ -0,0 +1,31 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2009 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Private structure for the private options. */ + +typedef struct { + uschar *server_secret; + uschar *client_secret; + uschar *client_name; +} auth_cram_md5_options_block; + +/* Data for reading the private options. */ + +extern optionlist auth_cram_md5_options[]; +extern int auth_cram_md5_options_count; + +/* Block containing default values. */ + +extern auth_cram_md5_options_block auth_cram_md5_option_defaults; + +/* The entry points for the mechanism */ + +extern void auth_cram_md5_init(auth_instance *); +extern int auth_cram_md5_server(auth_instance *, uschar *); +extern int auth_cram_md5_client(auth_instance *, void *, int, uschar *, int); + +/* End of cram_md5.h */ diff --git a/src/auths/cyrus_sasl.c b/src/auths/cyrus_sasl.c new file mode 100644 index 0000000..c8e2da5 --- /dev/null +++ b/src/auths/cyrus_sasl.c @@ -0,0 +1,513 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* This code was originally contributed by Matthew Byng-Maddick */ + +/* Copyright (c) A L Digital 2004 */ + +/* A generic (mechanism independent) Cyrus SASL authenticator. */ + + +#include "../exim.h" + + +/* We can't just compile this code and allow the library mechanism to omit the +functions if they are not wanted, because we need to have the Cyrus SASL header +available for compiling. Therefore, compile these functions only if +AUTH_CYRUS_SASL is defined. However, some compilers don't like compiling empty +modules, so keep them happy with a dummy when skipping the rest. Make it +reference itself to stop picky compilers complaining that it is unused, and put +in a dummy argument to stop even pickier compilers complaining about infinite +loops. */ + +#ifndef AUTH_CYRUS_SASL +static void dummy(int x); +static void dummy2(int x) { dummy(x-1); } +static void dummy(int x) { dummy2(x-1); } +#else + + +#include +#include "cyrus_sasl.h" + +/* Options specific to the cyrus_sasl authentication mechanism. */ + +optionlist auth_cyrus_sasl_options[] = { + { "server_hostname", opt_stringptr, + OPT_OFF(auth_cyrus_sasl_options_block, server_hostname) }, + { "server_mech", opt_stringptr, + OPT_OFF(auth_cyrus_sasl_options_block, server_mech) }, + { "server_realm", opt_stringptr, + OPT_OFF(auth_cyrus_sasl_options_block, server_realm) }, + { "server_service", opt_stringptr, + OPT_OFF(auth_cyrus_sasl_options_block, server_service) } +}; + +/* Size of the options list. An extern variable has to be used so that its +address can appear in the tables drtables.c. */ + +int auth_cyrus_sasl_options_count = + sizeof(auth_cyrus_sasl_options)/sizeof(optionlist); + +/* Default private options block for the cyrus_sasl authentication method. */ + +auth_cyrus_sasl_options_block auth_cyrus_sasl_option_defaults = { + US"smtp", /* server_service */ + US"$primary_hostname", /* server_hostname */ + NULL, /* server_realm */ + NULL /* server_mech */ +}; + + +#ifdef MACRO_PREDEF + +/* Dummy values */ +void auth_cyrus_sasl_init(auth_instance *ablock) {} +int auth_cyrus_sasl_server(auth_instance *ablock, uschar *data) {return 0;} +int auth_cyrus_sasl_client(auth_instance *ablock, void * sx, + int timeout, uschar *buffer, int buffsize) {return 0;} +gstring * auth_cyrus_sasl_version_report(gstring * g) {return NULL;} + +#else /*!MACRO_PREDEF*/ + + + + +/************************************************* +* Initialization entry point * +*************************************************/ + +/* Called for each instance, after its options have been read, to +enable consistency checks to be done, or anything else that needs +to be set up. */ + + +/* Auxiliary function, passed in data to sasl_server_init(). */ + +static int +mysasl_config(void *context, const char *plugin_name, const char *option, + const char **result, unsigned int *len) +{ +if (context && !strcmp(option, "mech_list")) + { + *result = context; + if (len) *len = strlen(*result); + return SASL_OK; + } +return SASL_FAIL; +} + +/* Here's the real function */ + +void +auth_cyrus_sasl_init(auth_instance *ablock) +{ +auth_cyrus_sasl_options_block *ob = + (auth_cyrus_sasl_options_block *)(ablock->options_block); +const uschar *list, *listptr, *buffer; +int rc, i; +unsigned int len; +rmark rs_point; +uschar *expanded_hostname; +char *realm_expanded; + +sasl_conn_t *conn; +sasl_callback_t cbs[] = { + {SASL_CB_GETOPT, NULL, NULL }, + {SASL_CB_LIST_END, NULL, NULL}}; + +/* default the mechanism to our "public name" */ + +if (!ob->server_mech) ob->server_mech = string_copy(ablock->public_name); + +if (!(expanded_hostname = expand_string(ob->server_hostname))) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s authenticator: " + "couldn't expand server_hostname [%s]: %s", + ablock->name, ob->server_hostname, expand_string_message); + +realm_expanded = NULL; +if ( ob->server_realm + && !(realm_expanded = CS expand_string(ob->server_realm))) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s authenticator: " + "couldn't expand server_realm [%s]: %s", + ablock->name, ob->server_realm, expand_string_message); + +/* we're going to initialise the library to check that there is an +authenticator of type whatever mechanism we're using */ + +cbs[0].proc = (int(*)(void)) &mysasl_config; +cbs[0].context = ob->server_mech; + +if ((rc = sasl_server_init(cbs, "exim")) != SASL_OK) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s authenticator: " + "couldn't initialise Cyrus SASL library.", ablock->name); + +if ((rc = sasl_server_new(CS ob->server_service, CS expanded_hostname, + realm_expanded, NULL, NULL, NULL, 0, &conn)) != SASL_OK) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s authenticator: " + "couldn't initialise Cyrus SASL server connection.", ablock->name); + +if ((rc = sasl_listmech(conn, NULL, "", ":", "", CCSS &list, &len, &i)) != SASL_OK) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s authenticator: " + "couldn't get Cyrus SASL mechanism list.", ablock->name); + +i = ':'; +listptr = list; + +HDEBUG(D_auth) + { + debug_printf("Initialised Cyrus SASL service=\"%s\" fqdn=\"%s\" realm=\"%s\"\n", + ob->server_service, expanded_hostname, realm_expanded); + debug_printf("Cyrus SASL knows mechanisms: %s\n", list); + } + +/* the store_get / store_reset mechanism is hierarchical + the hierarchy is stored for us behind our back. This point + creates a hierarchy point for this function. */ + +rs_point = store_mark(); + +/* loop until either we get to the end of the list, or we match the +public name of this authenticator */ + +while ( (buffer = string_nextinlist(&listptr, &i, NULL, 0)) + && strcmpic(buffer,ob->server_mech) ); + +if (!buffer) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s authenticator: " + "Cyrus SASL doesn't know about mechanism %s.", ablock->name, ob->server_mech); + +store_reset(rs_point); + +HDEBUG(D_auth) debug_printf("Cyrus SASL driver %s: %s initialised\n", ablock->name, ablock->public_name); + +/* make sure that if we get here then we're allowed to advertise. */ +ablock->server = TRUE; + +sasl_dispose(&conn); +sasl_done(); +} + +/************************************************* +* Server entry point * +*************************************************/ + +/* For interface, see auths/README */ + +/* note, we don't care too much about memory allocation in this, because this is entirely +within a shortlived child */ + +int +auth_cyrus_sasl_server(auth_instance *ablock, uschar *data) +{ +auth_cyrus_sasl_options_block *ob = + (auth_cyrus_sasl_options_block *)(ablock->options_block); +uschar *output, *out2, *input, *clear, *hname; +uschar *debug = NULL; /* Stops compiler complaining */ +sasl_callback_t cbs[] = {{SASL_CB_LIST_END, NULL, NULL}}; +sasl_conn_t *conn; +char * realm_expanded = NULL; +int rc, firsttime = 1, clen, *negotiated_ssf_ptr = NULL, negotiated_ssf; +unsigned int inlen, outlen; + +input = data; +inlen = Ustrlen(data); + +HDEBUG(D_auth) debug = string_copy(data); + +hname = expand_string(ob->server_hostname); +if (hname && ob->server_realm) + realm_expanded = CS expand_string(ob->server_realm); +if (!hname || !realm_expanded && ob->server_realm) + { + auth_defer_msg = expand_string_message; + return DEFER; + } + +if (inlen) + { + if ((clen = b64decode(input, &clear)) < 0) + return BAD64; + input = clear; + inlen = clen; + } + +if ((rc = sasl_server_init(cbs, "exim")) != SASL_OK) + { + auth_defer_msg = US"couldn't initialise Cyrus SASL library"; + return DEFER; + } + +rc = sasl_server_new(CS ob->server_service, CS hname, realm_expanded, NULL, + NULL, NULL, 0, &conn); + +HDEBUG(D_auth) + debug_printf("Initialised Cyrus SASL server connection; service=\"%s\" fqdn=\"%s\" realm=\"%s\"\n", + ob->server_service, hname, realm_expanded); + +if (rc != SASL_OK ) + { + auth_defer_msg = US"couldn't initialise Cyrus SASL connection"; + sasl_done(); + return DEFER; + } + +if (tls_in.cipher) + { + if ((rc = sasl_setprop(conn, SASL_SSF_EXTERNAL, (sasl_ssf_t *) &tls_in.bits)) != SASL_OK) + { + HDEBUG(D_auth) debug_printf("Cyrus SASL EXTERNAL SSF set %d failed: %s\n", + tls_in.bits, sasl_errstring(rc, NULL, NULL)); + auth_defer_msg = US"couldn't set Cyrus SASL EXTERNAL SSF"; + sasl_done(); + return DEFER; + } + else + HDEBUG(D_auth) debug_printf("Cyrus SASL set EXTERNAL SSF to %d\n", tls_in.bits); + + /*XXX Set channel-binding here with sasl_channel_binding_t / SASL_CHANNEL_BINDING + Unclear what the "name" element does though, ditto the "critical" flag. */ + } +else + HDEBUG(D_auth) debug_printf("Cyrus SASL: no TLS, no EXTERNAL SSF set\n"); + +/* So sasl_setprop() documents non-shorted IPv6 addresses which is incredibly +annoying; looking at cyrus-imapd-2.3.x source, the IP address is constructed +with their iptostring() function, which just wraps +getnameinfo(..., NI_NUMERICHOST|NI_NUMERICSERV), which is equivalent to the +inet_ntop which we wrap in our host_ntoa() function. + +So the docs are too strict and we shouldn't worry about :: contractions. */ + +/* Set properties for remote and local host-ip;port */ +for (int i = 0; i < 2; ++i) + { + int propnum; + const uschar * label; + uschar * address_port; + const char *s_err; + + if (i) + { + propnum = SASL_IPREMOTEPORT; + label = CUS"peer"; + address_port = string_sprintf("%s;%d", + sender_host_address, sender_host_port); + } + else + { + propnum = SASL_IPLOCALPORT; + label = CUS"local"; + address_port = string_sprintf("%s;%d", interface_address, interface_port); + } + + if ((rc = sasl_setprop(conn, propnum, address_port)) != SASL_OK) + { + HDEBUG(D_auth) + { + s_err = sasl_errdetail(conn); + debug_printf("Failed to set %s SASL property: [%d] %s\n", + label, rc, s_err ? s_err : ""); + } + break; + } + HDEBUG(D_auth) debug_printf("Cyrus SASL set %s hostport to: %s\n", + label, address_port); + } + +for (rc = SASL_CONTINUE; rc == SASL_CONTINUE; ) + { + if (firsttime) + { + firsttime = 0; + HDEBUG(D_auth) debug_printf("Calling sasl_server_start(%s,\"%s\")\n", ob->server_mech, debug); + rc = sasl_server_start(conn, CS ob->server_mech, inlen ? CS input : NULL, inlen, + CCSS &output, &outlen); + } + else + { + /* auth_get_data() takes a length-specfied block of binary + which can include zeroes; no terminating NUL is needed */ + + if ((rc = auth_get_data(&input, output, outlen)) != OK) + { + /* we couldn't get the data, so free up the library before + returning whatever error we get */ + sasl_dispose(&conn); + sasl_done(); + return rc; + } + inlen = Ustrlen(input); + + HDEBUG(D_auth) debug = string_copy(input); + if (inlen) + { + if ((clen = b64decode(input, &clear)) < 0) + { + sasl_dispose(&conn); + sasl_done(); + return BAD64; + } + input = clear; + inlen = clen; + } + + HDEBUG(D_auth) debug_printf("Calling sasl_server_step(\"%s\")\n", debug); + rc = sasl_server_step(conn, CS input, inlen, CCSS &output, &outlen); + } + + if (rc == SASL_BADPROT) + { + sasl_dispose(&conn); + sasl_done(); + return UNEXPECTED; + } + if (rc == SASL_CONTINUE) + continue; + + /* Get the username and copy it into $auth1 and $1. The former is now the + preferred variable; the latter is the original variable. */ + + if ((sasl_getprop(conn, SASL_USERNAME, (const void **)&out2)) != SASL_OK) + { + HDEBUG(D_auth) + debug_printf("Cyrus SASL library will not tell us the username: %s\n", + sasl_errstring(rc, NULL, NULL)); + log_write(0, LOG_REJECT, "%s authenticator (%s): " + "Cyrus SASL username fetch problem: %s", ablock->name, ob->server_mech, + sasl_errstring(rc, NULL, NULL)); + sasl_dispose(&conn); + sasl_done(); + return FAIL; + } + auth_vars[0] = expand_nstring[1] = string_copy(out2); + expand_nlength[1] = Ustrlen(out2); + expand_nmax = 1; + + switch (rc) + { + case SASL_FAIL: case SASL_BUFOVER: case SASL_BADMAC: case SASL_BADAUTH: + case SASL_NOAUTHZ: case SASL_ENCRYPT: case SASL_EXPIRED: + case SASL_DISABLED: case SASL_NOUSER: + /* these are considered permanent failure codes */ + HDEBUG(D_auth) + debug_printf("Cyrus SASL permanent failure %d (%s)\n", rc, sasl_errstring(rc, NULL, NULL)); + log_write(0, LOG_REJECT, "%s authenticator (%s): " + "Cyrus SASL permanent failure: %s", ablock->name, ob->server_mech, + sasl_errstring(rc, NULL, NULL)); + sasl_dispose(&conn); + sasl_done(); + return FAIL; + + case SASL_NOMECH: + /* this is a temporary failure, because the mechanism is not + available for this user. If it wasn't available at all, we + shouldn't have got here in the first place... */ + + HDEBUG(D_auth) + debug_printf("Cyrus SASL temporary failure %d (%s)\n", rc, sasl_errstring(rc, NULL, NULL)); + auth_defer_msg = + string_sprintf("Cyrus SASL: mechanism %s not available", ob->server_mech); + sasl_dispose(&conn); + sasl_done(); + return DEFER; + + case SASL_OK: + HDEBUG(D_auth) + debug_printf("Cyrus SASL %s authentication succeeded for %s\n", + ob->server_mech, auth_vars[0]); + + if ((rc = sasl_getprop(conn, SASL_SSF, (const void **)(&negotiated_ssf_ptr)))!= SASL_OK) + { + HDEBUG(D_auth) + debug_printf("Cyrus SASL library will not tell us the SSF: %s\n", + sasl_errstring(rc, NULL, NULL)); + log_write(0, LOG_REJECT, "%s authenticator (%s): " + "Cyrus SASL SSF value not available: %s", ablock->name, ob->server_mech, + sasl_errstring(rc, NULL, NULL)); + sasl_dispose(&conn); + sasl_done(); + return FAIL; + } + negotiated_ssf = *negotiated_ssf_ptr; + HDEBUG(D_auth) + debug_printf("Cyrus SASL %s negotiated SSF: %d\n", ob->server_mech, negotiated_ssf); + if (negotiated_ssf > 0) + { + HDEBUG(D_auth) + debug_printf("Exim does not implement SASL wrapping (needed for SSF %d).\n", negotiated_ssf); + log_write(0, LOG_REJECT, "%s authenticator (%s): " + "Cyrus SASL SSF %d not supported by Exim", ablock->name, ob->server_mech, negotiated_ssf); + sasl_dispose(&conn); + sasl_done(); + return FAIL; + } + + /* close down the connection, freeing up library's memory */ + sasl_dispose(&conn); + sasl_done(); + + /* Expand server_condition as an authorization check */ + return auth_check_serv_cond(ablock); + + default: + /* Anything else is a temporary failure, and we'll let SASL print out + * the error string for us + */ + HDEBUG(D_auth) + debug_printf("Cyrus SASL temporary failure %d (%s)\n", rc, sasl_errstring(rc, NULL, NULL)); + auth_defer_msg = + string_sprintf("Cyrus SASL: %s", sasl_errstring(rc, NULL, NULL)); + sasl_dispose(&conn); + sasl_done(); + return DEFER; + } + } +/* NOTREACHED */ +return 0; /* Stop compiler complaints */ +} + +/************************************************* +* Diagnostic API * +*************************************************/ + +gstring * +auth_cyrus_sasl_version_report(gstring * g) +{ +const char * implementation, * version; +sasl_version_info(&implementation, &version, NULL, NULL, NULL, NULL); +g = string_fmt_append(g, + "Library version: Cyrus SASL: Compile: %d.%d.%d\n" + " Runtime: %s [%s]\n", + SASL_VERSION_MAJOR, SASL_VERSION_MINOR, SASL_VERSION_STEP, + version, implementation); +return g; +} + +/************************************************* +* Client entry point * +*************************************************/ + +/* For interface, see auths/README */ + +int +auth_cyrus_sasl_client( + auth_instance *ablock, /* authenticator block */ + void * sx, /* connexction */ + int timeout, /* command timeout */ + uschar *buffer, /* for reading response */ + int buffsize) /* size of buffer */ +{ +/* We don't support clients (yet) in this implementation of cyrus_sasl */ +return FAIL; +} + +#endif /*!MACRO_PREDEF*/ +#endif /* AUTH_CYRUS_SASL */ + +/* End of cyrus_sasl.c */ diff --git a/src/auths/cyrus_sasl.h b/src/auths/cyrus_sasl.h new file mode 100644 index 0000000..6cf8834 --- /dev/null +++ b/src/auths/cyrus_sasl.h @@ -0,0 +1,36 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2022 */ +/* Copyright (c) University of Cambridge 1995 - 2012 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Copyright (c) A L Digital Ltd 2004 */ + +/* Private structure for the private options. */ + +typedef struct { + uschar *server_service; + uschar *server_hostname; + uschar *server_realm; + uschar *server_mech; +} auth_cyrus_sasl_options_block; + +/* Data for reading the private options. */ + +extern optionlist auth_cyrus_sasl_options[]; +extern int auth_cyrus_sasl_options_count; + +/* Block containing default values. */ + +extern auth_cyrus_sasl_options_block auth_cyrus_sasl_option_defaults; + +/* The entry points for the mechanism */ + +extern void auth_cyrus_sasl_init(auth_instance *); +extern int auth_cyrus_sasl_server(auth_instance *, uschar *); +extern int auth_cyrus_sasl_client(auth_instance *, void *, int, uschar *, int); +extern gstring * auth_cyrus_sasl_version_report(gstring *); + +/* End of cyrus_sasl.h */ diff --git a/src/auths/dovecot.c b/src/auths/dovecot.c new file mode 100644 index 0000000..5d77133 --- /dev/null +++ b/src/auths/dovecot.c @@ -0,0 +1,530 @@ +/* + * Copyright (c) The Exim Maintainers 2006 - 2022 + * Copyright (c) 2004 Andrey Panin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + +/* A number of modifications have been made to the original code. Originally I +commented them specially, but now they are getting quite extensive, so I have +ceased doing that. The biggest change is to use unbuffered I/O on the socket +because using C buffered I/O gives problems on some operating systems. PH */ + +/* Protocol specifications: + * Dovecot 1, protocol version 1.1 + * http://wiki.dovecot.org/Authentication%20Protocol + * + * Dovecot 2, protocol version 1.1 + * http://wiki2.dovecot.org/Design/AuthProtocol + */ + +#include "../exim.h" +#include "dovecot.h" + +#define VERSION_MAJOR 1 +#define VERSION_MINOR 0 + +/* http://wiki.dovecot.org/Authentication%20Protocol +"The maximum line length isn't defined, + but it's currently expected to fit into 8192 bytes" +*/ +#define DOVECOT_AUTH_MAXLINELEN 8192 + +/* This was hard-coded as 8. +AUTH req C->S sends {"AUTH", id, mechanism, service } + params, 5 defined for +Dovecot 1; Dovecot 2 (same protocol version) defines 9. + +Master->Server sends {"USER", id, userid} + params, 6 defined. +Server->Client only gives {"OK", id} + params, unspecified, only 1 guaranteed. + +We only define here to accept S->C; max seen is 3+, plus the two +for the command and id, where unspecified might include _at least_ user=... + +So: allow for more fields than we ever expect to see, while aware that count +can go up without changing protocol version. +The cost is the length of an array of pointers on the stack. +*/ +#define DOVECOT_AUTH_MAXFIELDCOUNT 16 + +/* Options specific to the authentication mechanism. */ +optionlist auth_dovecot_options[] = { + { "server_socket", opt_stringptr, OPT_OFF(auth_dovecot_options_block, server_socket) }, +/*{ "server_tls", opt_bool, OPT_OFF(auth_dovecot_options_block, server_tls) },*/ +}; + +/* Size of the options list. An extern variable has to be used so that its +address can appear in the tables drtables.c. */ + +int auth_dovecot_options_count = nelem(auth_dovecot_options); + +/* Default private options block for the authentication method. */ + +auth_dovecot_options_block auth_dovecot_option_defaults = { + .server_socket = NULL, +/* .server_tls = FALSE,*/ +}; + + + + +#ifdef MACRO_PREDEF + +/* Dummy values */ +void auth_dovecot_init(auth_instance *ablock) {} +int auth_dovecot_server(auth_instance *ablock, uschar *data) {return 0;} +int auth_dovecot_client(auth_instance *ablock, void * sx, + int timeout, uschar *buffer, int buffsize) {return 0;} + +#else /*!MACRO_PREDEF*/ + + +/* Static variables for reading from the socket */ + +static uschar sbuffer[256]; +static int socket_buffer_left; + + + +/************************************************* + * Initialization entry point * + *************************************************/ + +/* Called for each instance, after its options have been read, to +enable consistency checks to be done, or anything else that needs +to be set up. */ + +void auth_dovecot_init(auth_instance *ablock) +{ +auth_dovecot_options_block *ob = + (auth_dovecot_options_block *)(ablock->options_block); + +if (!ablock->public_name) ablock->public_name = ablock->name; +if (ob->server_socket) ablock->server = TRUE; +ablock->client = FALSE; +} + +/************************************************* + * "strcut" to split apart server lines * + *************************************************/ + +/* Dovecot auth protocol uses TAB \t as delimiter; a line consists +of a command-name, TAB, and then any parameters, each separated by a TAB. +A parameter can be param=value or a bool, just param. + +This function modifies the original str in-place, inserting NUL characters. +It initialises ptrs entries, setting all to NULL and only setting +non-NULL N entries, where N is the return value, the number of fields seen +(one more than the number of tabs). + +Note that the return value will always be at least 1, is the count of +actual fields (so last valid offset into ptrs is one less). +*/ + +static int +strcut(uschar *str, uschar **ptrs, int nptrs) +{ +uschar *last_sub_start = str; +int n; + +for (n = 0; n < nptrs; n++) + ptrs[n] = NULL; +n = 1; + +while (*str) + if (*str++ == '\t') + if (n++ <= nptrs) + { + *ptrs++ = last_sub_start; + last_sub_start = str; + str[-1] = '\0'; + } + +/* It's acceptable for the string to end with a tab character. We see +this in AUTH PLAIN without an initial response from the client, which +causing us to send "334 " and get the data from the client. */ +if (n <= nptrs) + *ptrs = last_sub_start; +else + { + HDEBUG(D_auth) + debug_printf("dovecot: warning: too many results from tab-splitting;" + " saw %d fields, room for %d\n", n, nptrs); + n = nptrs; + } + +return n <= nptrs ? n : nptrs; +} + +static void debug_strcut(uschar **ptrs, int nlen, int alen) ARG_UNUSED; +static void +debug_strcut(uschar **ptrs, int nlen, int alen) +{ +int i; +debug_printf("%d read but unreturned bytes; strcut() gave %d results: ", + socket_buffer_left, nlen); +for (i = 0; i < nlen; i++) + debug_printf(" {%s}", ptrs[i]); +if (nlen < alen) + debug_printf(" last is %s\n", ptrs[i] ? ptrs[i] : US""); +else + debug_printf(" (max for capacity)\n"); +} + +#define CHECK_COMMAND(str, arg_min, arg_max) do { \ + if (strcmpic(US(str), args[0]) != 0) \ + goto out; \ + if (nargs - 1 < (arg_min)) \ + goto out; \ + if ( (arg_max != -1) && (nargs - 1 > (arg_max)) ) \ + goto out; \ +} while (0) + +#define OUT(msg) do { \ + auth_defer_msg = (US msg); \ + goto out; \ +} while(0) + + + +/************************************************* +* "fgets" to read directly from socket * +*************************************************/ + +/* Added by PH after a suggestion by Steve Usher because the previous use of +C-style buffered I/O gave trouble. */ + +static uschar * +dc_gets(uschar *s, int n, client_conn_ctx * cctx) +{ +int p = 0; +int count = 0; + +for (;;) + { + if (socket_buffer_left == 0) + { + if ((socket_buffer_left = +#ifndef DISABLE_TLS + cctx->tls_ctx ? tls_read(cctx->tls_ctx, sbuffer, sizeof(sbuffer)) : +#endif + read(cctx->sock, sbuffer, sizeof(sbuffer))) <= 0) + if (count == 0) + return NULL; + else + break; + p = 0; + } + + while (p < socket_buffer_left) + { + if (count >= n - 1) break; + s[count++] = sbuffer[p]; + if (sbuffer[p++] == '\n') break; + } + + memmove(sbuffer, sbuffer + p, socket_buffer_left - p); + socket_buffer_left -= p; + + if (s[count-1] == '\n' || count >= n - 1) break; + } + +s[count] = '\0'; +return s; +} + + + + +/************************************************* +* Server entry point * +*************************************************/ + +int +auth_dovecot_server(auth_instance * ablock, uschar * data) +{ +auth_dovecot_options_block *ob = + (auth_dovecot_options_block *) ablock->options_block; +uschar buffer[DOVECOT_AUTH_MAXLINELEN]; +uschar *args[DOVECOT_AUTH_MAXFIELDCOUNT]; +uschar *auth_command; +uschar *auth_extra_data = US""; +uschar *p; +int nargs, tmp; +int crequid = 1, ret = DEFER; +host_item host; +client_conn_ctx cctx = {.sock = -1, .tls_ctx = NULL}; +BOOL found = FALSE, have_mech_line = FALSE; + +HDEBUG(D_auth) debug_printf("dovecot authentication\n"); + +if (!data) + { + ret = FAIL; + goto out; + } + +/*XXX timeout? */ +cctx.sock = ip_streamsocket(ob->server_socket, &auth_defer_msg, 5, &host); +if (cctx.sock < 0) + goto out; + +#ifdef notdef +# ifndef DISABLE_TLS +if (ob->server_tls) + { + union sockaddr_46 interface_sock; + EXIM_SOCKLEN_T size = sizeof(interface_sock); + smtp_connect_args conn_args = { .host = &host }; + tls_support tls_dummy = { .sni = NULL }; + uschar * errstr; + + if (getsockname(cctx->sock, (struct sockaddr *) &interface_sock, &size) == 0) + conn_args.sending_ip_address = host_ntoa(-1, &interface_sock, NULL, NULL); + else + { + *errmsg = string_sprintf("getsockname failed: %s", strerror(errno)); + goto bad; + } + + if (!tls_client_start(&cctx, &conn_args, NULL, &tls_dummy, &errstr)) + { + auth_defer_msg = string_sprintf("TLS connect failed: %s", errstr); + goto out; + } + } +# endif +#endif + +auth_defer_msg = US"authentication socket protocol error"; + +socket_buffer_left = 0; /* Global, used to read more than a line but return by line */ +for (;;) + { +debug_printf("%s %d\n", __FUNCTION__, __LINE__); + if (!dc_gets(buffer, sizeof(buffer), &cctx)) + OUT("authentication socket read error or premature eof"); +debug_printf("%s %d\n", __FUNCTION__, __LINE__); + p = buffer + Ustrlen(buffer) - 1; + if (*p != '\n') + OUT("authentication socket protocol line too long"); + + *p = '\0'; + HDEBUG(D_auth) debug_printf("received: '%s'\n", buffer); + + nargs = strcut(buffer, args, nelem(args)); + + HDEBUG(D_auth) debug_strcut(args, nargs, nelem(args)); + + /* Code below rewritten by Kirill Miazine (km@krot.org). Only check commands that + Exim will need. Original code also failed if Dovecot server sent unknown + command. E.g. COOKIE in version 1.1 of the protocol would cause troubles. */ + /* pdp: note that CUID is a per-connection identifier sent by the server, + which increments at server discretion. + By contrast, the "id" field of the protocol is a connection-specific request + identifier, which needs to be unique per request from the client and is not + connected to the CUID value, so we ignore CUID from server. It's purely for + diagnostics. */ + + if (Ustrcmp(args[0], US"VERSION") == 0) + { + CHECK_COMMAND("VERSION", 2, 2); + if (Uatoi(args[1]) != VERSION_MAJOR) + OUT("authentication socket protocol version mismatch"); + } + else if (Ustrcmp(args[0], US"MECH") == 0) + { + CHECK_COMMAND("MECH", 1, INT_MAX); + have_mech_line = TRUE; + if (strcmpic(US args[1], ablock->public_name) == 0) + found = TRUE; + } + else if (Ustrcmp(args[0], US"SPID") == 0) + { + /* Unfortunately the auth protocol handshake wasn't designed well + to differentiate between auth-client/userdb/master. auth-userdb + and auth-master send VERSION + SPID lines only and nothing + afterwards, while auth-client sends VERSION + MECH + SPID + + CUID + more. The simplest way that we can determine if we've + connected to the correct socket is to see if MECH line exists or + not (alternatively we'd have to have a small timeout after SPID + to see if CUID is sent or not). */ + + if (!have_mech_line) + OUT("authentication socket type mismatch" + " (connected to auth-master instead of auth-client)"); + } + else if (Ustrcmp(args[0], US"DONE") == 0) + { + CHECK_COMMAND("DONE", 0, 0); + break; + } + } + +if (!found) + { + auth_defer_msg = string_sprintf( + "Dovecot did not advertise mechanism \"%s\" to us", ablock->public_name); + goto out; + } + +/* Added by PH: data must not contain tab (as it is +b64 it shouldn't, but check for safety). */ + +if (Ustrchr(data, '\t') != NULL) + { + ret = FAIL; + goto out; + } + +/* Added by PH: extra fields when TLS is in use or if the TCP/IP +connection is local. */ + +if (tls_in.cipher) + auth_extra_data = string_sprintf("secured\t%s%s", + tls_in.certificate_verified ? "valid-client-cert" : "", + tls_in.certificate_verified ? "\t" : ""); + +else if ( interface_address + && Ustrcmp(sender_host_address, interface_address) == 0) + auth_extra_data = US"secured\t"; + + +/**************************************************************************** +The code below was the original code here. It didn't work. A reading of the +file auth-protocol.txt.gz that came with Dovecot 1.0_beta8 indicated that +this was not right. Maybe something changed. I changed it to move the +service indication into the AUTH command, and it seems to be better. PH + +fprintf(f, "VERSION\t%d\t%d\r\nSERVICE\tSMTP\r\nCPID\t%d\r\n" + "AUTH\t%d\t%s\trip=%s\tlip=%s\tresp=%s\r\n", + VERSION_MAJOR, VERSION_MINOR, getpid(), cuid, + ablock->public_name, sender_host_address, interface_address, + data ? CS data : ""); + +Subsequently, the command was modified to add "secured" and "valid-client- +cert" when relevant. +****************************************************************************/ + +auth_command = string_sprintf("VERSION\t%d\t%d\nCPID\t%d\n" + "AUTH\t%d\t%s\tservice=smtp\t%srip=%s\tlip=%s\tnologin\tresp=%s\n", + VERSION_MAJOR, VERSION_MINOR, getpid(), crequid, + ablock->public_name, auth_extra_data, sender_host_address, + interface_address, data); + +if (( +#ifndef DISABLE_TLS + cctx.tls_ctx ? tls_write(cctx.tls_ctx, auth_command, Ustrlen(auth_command), FALSE) : +#endif + write(cctx.sock, auth_command, Ustrlen(auth_command))) < 0) + HDEBUG(D_auth) debug_printf("error sending auth_command: %s\n", + strerror(errno)); + +HDEBUG(D_auth) debug_printf("sent: '%s'\n", auth_command); + +while (1) + { + uschar *temp; + uschar *auth_id_pre = NULL; + + if (!dc_gets(buffer, sizeof(buffer), &cctx)) + { + auth_defer_msg = US"authentication socket read error or premature eof"; + goto out; + } + + buffer[Ustrlen(buffer) - 1] = 0; + HDEBUG(D_auth) debug_printf("received: '%s'\n", buffer); + nargs = strcut(buffer, args, nelem(args)); + HDEBUG(D_auth) debug_strcut(args, nargs, nelem(args)); + + if (Uatoi(args[1]) != crequid) + OUT("authentication socket connection id mismatch"); + + switch (toupper(*args[0])) + { + case 'C': + CHECK_COMMAND("CONT", 1, 2); + + if ((tmp = auth_get_no64_data(&data, US args[2])) != OK) + { + ret = tmp; + goto out; + } + + /* Added by PH: data must not contain tab (as it is + b64 it shouldn't, but check for safety). */ + + if (Ustrchr(data, '\t') != NULL) + { + ret = FAIL; + goto out; + } + + temp = string_sprintf("CONT\t%d\t%s\n", crequid, data); + if (( +#ifndef DISABLE_TLS + cctx.tls_ctx ? tls_write(cctx.tls_ctx, temp, Ustrlen(temp), FALSE) : +#endif + write(cctx.sock, temp, Ustrlen(temp))) < 0) + OUT("authentication socket write error"); + break; + + case 'F': + CHECK_COMMAND("FAIL", 1, -1); + + for (int i = 2; i < nargs && !auth_id_pre; i++) + if (Ustrncmp(args[i], US"user=", 5) == 0) + { + auth_id_pre = args[i] + 5; + expand_nstring[1] = auth_vars[0] = string_copy(auth_id_pre); /* PH */ + expand_nlength[1] = Ustrlen(auth_id_pre); + expand_nmax = 1; + } + ret = FAIL; + goto out; + + case 'O': + CHECK_COMMAND("OK", 2, -1); + + /* Search for the "user=$USER" string in the args array + and return the proper value. */ + + for (int i = 2; i < nargs && !auth_id_pre; i++) + if (Ustrncmp(args[i], US"user=", 5) == 0) + { + auth_id_pre = args[i] + 5; + expand_nstring[1] = auth_vars[0] = string_copy(auth_id_pre); /* PH */ + expand_nlength[1] = Ustrlen(auth_id_pre); + expand_nmax = 1; + } + + if (!auth_id_pre) + OUT("authentication socket protocol error, username missing"); + + auth_defer_msg = NULL; + ret = OK; + /* fallthrough */ + + default: + goto out; + } + } + +out: +/* close the socket used by dovecot */ +#ifndef DISABLE_TLS +if (cctx.tls_ctx) + tls_close(cctx.tls_ctx, TRUE); +#endif +if (cctx.sock >= 0) + close(cctx.sock); + +/* Expand server_condition as an authorization check */ +return ret == OK ? auth_check_serv_cond(ablock) : ret; +} + + +#endif /*!MACRO_PREDEF*/ diff --git a/src/auths/dovecot.h b/src/auths/dovecot.h new file mode 100644 index 0000000..bfe1f07 --- /dev/null +++ b/src/auths/dovecot.h @@ -0,0 +1,30 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2009 */ +/* Copyright (c) The Exim Maintainters 2020 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Private structure for the private options. */ + +typedef struct { + uschar * server_socket; + BOOL server_tls; +} auth_dovecot_options_block; + +/* Data for reading the private options. */ + +extern optionlist auth_dovecot_options[]; +extern int auth_dovecot_options_count; + +/* Block containing default values. */ + +extern auth_dovecot_options_block auth_dovecot_option_defaults; + +/* The entry points for the mechanism */ + +extern void auth_dovecot_init(auth_instance *); +extern int auth_dovecot_server(auth_instance *, uschar *); + +/* End of dovecot.h */ diff --git a/src/auths/external.c b/src/auths/external.c new file mode 100644 index 0000000..7e7fca8 --- /dev/null +++ b/src/auths/external.c @@ -0,0 +1,155 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) Jeremy Harris 2019-2020 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* This file provides an Exim authenticator driver for +a server to verify a client SSL certificate, using the EXTERNAL +method defined in RFC 4422 Appendix A. +*/ + + +#include "../exim.h" +#include "external.h" + +/* Options specific to the external authentication mechanism. */ + +optionlist auth_external_options[] = { + { "client_send", opt_stringptr, OPT_OFF(auth_external_options_block, client_send) }, + { "server_param2", opt_stringptr, OPT_OFF(auth_external_options_block, server_param2) }, + { "server_param3", opt_stringptr, OPT_OFF(auth_external_options_block, server_param3) }, +}; + +/* Size of the options list. An extern variable has to be used so that its +address can appear in the tables drtables.c. */ + +int auth_external_options_count = nelem(auth_external_options); + +/* Default private options block for the authentication method. */ + +auth_external_options_block auth_external_option_defaults = { + .server_param2 = NULL, + .server_param3 = NULL, + + .client_send = NULL, +}; + + +#ifdef MACRO_PREDEF + +/* Dummy values */ +void auth_external_init(auth_instance *ablock) {} +int auth_external_server(auth_instance *ablock, uschar *data) {return 0;} +int auth_external_client(auth_instance *ablock, void * sx, + int timeout, uschar *buffer, int buffsize) {return 0;} + +#else /*!MACRO_PREDEF*/ + + + + +/************************************************* +* Initialization entry point * +*************************************************/ + +/* Called for each instance, after its options have been read, to +enable consistency checks to be done, or anything else that needs +to be set up. */ + +void +auth_external_init(auth_instance *ablock) +{ +auth_external_options_block * ob = (auth_external_options_block *)ablock->options_block; +if (!ablock->public_name) ablock->public_name = ablock->name; +if (ablock->server_condition) ablock->server = TRUE; +if (ob->client_send) ablock->client = TRUE; +} + + + +/************************************************* +* Server entry point * +*************************************************/ + +/* For interface, see auths/README */ + +int +auth_external_server(auth_instance * ablock, uschar * data) +{ +auth_external_options_block * ob = (auth_external_options_block *)ablock->options_block; +int rc; + +/* If data was supplied on the AUTH command, decode it, and split it up into +multiple items at binary zeros. The strings are put into $auth1, $auth2, etc, +up to a maximum. To retain backwards compatibility, they are also put int $1, +$2, etc. If the data consists of the string "=" it indicates a single, empty +string. */ + +if (*data) + if ((rc = auth_read_input(data)) != OK) + return rc; + +/* Now go through the list of prompt strings. Skip over any whose data has +already been provided as part of the AUTH command. For the rest, send them +out as prompts, and get a data item back. If the data item is "*", abandon the +authentication attempt. Otherwise, split it into items as above. */ + +if (expand_nmax == 0) /* skip if rxd data */ + if ((rc = auth_prompt(CUS"")) != OK) + return rc; + +if (ob->server_param2) + { + uschar * s = expand_string(ob->server_param2); + auth_vars[expand_nmax] = s; + expand_nstring[++expand_nmax] = s; + expand_nlength[expand_nmax] = Ustrlen(s); + if (ob->server_param3) + { + s = expand_string(ob->server_param3); + auth_vars[expand_nmax] = s; + expand_nstring[++expand_nmax] = s; + expand_nlength[expand_nmax] = Ustrlen(s); + } + } + +return auth_check_serv_cond(ablock); +} + + + +/************************************************* +* Client entry point * +*************************************************/ + +/* For interface, see auths/README */ + +int +auth_external_client( + auth_instance *ablock, /* authenticator block */ + void * sx, /* smtp connextion */ + int timeout, /* command timeout */ + uschar *buffer, /* buffer for reading response */ + int buffsize) /* size of buffer */ +{ +auth_external_options_block *ob = + (auth_external_options_block *)(ablock->options_block); +const uschar * text = ob->client_send; +int rc; + +/* We output an AUTH command with one expanded argument, the client_send option */ + +if ((rc = auth_client_item(sx, ablock, &text, AUTH_ITEM_FIRST | AUTH_ITEM_LAST, + timeout, buffer, buffsize)) != OK) + return rc == DEFER ? FAIL : rc; + +if (text) auth_vars[0] = string_copy(text); +return OK; +} + + + +#endif /*!MACRO_PREDEF*/ +/* End of external.c */ diff --git a/src/auths/external.h b/src/auths/external.h new file mode 100644 index 0000000..7d43650 --- /dev/null +++ b/src/auths/external.h @@ -0,0 +1,32 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) Jeremy Harris 2019 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Private structure for the private options. */ + +typedef struct { + uschar * server_param2; + uschar * server_param3; + + uschar * client_send; +} auth_external_options_block; + +/* Data for reading the private options. */ + +extern optionlist auth_external_options[]; +extern int auth_external_options_count; + +/* Block containing default values. */ + +extern auth_external_options_block auth_external_option_defaults; + +/* The entry points for the mechanism */ + +extern void auth_external_init(auth_instance *); +extern int auth_external_server(auth_instance *, uschar *); +extern int auth_external_client(auth_instance *, void *, int, uschar *, int); + +/* End of external.h */ diff --git a/src/auths/get_data.c b/src/auths/get_data.c new file mode 100644 index 0000000..e0d79db --- /dev/null +++ b/src/auths/get_data.c @@ -0,0 +1,259 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* Copyright (c) The Exim Maintainers 2020 - 2021 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" + + +/**************************************************************** +* Decode and split the argument of an AUTH command * +****************************************************************/ + +/* If data was supplied on the AUTH command, decode it, and split it up into +multiple items at binary zeros. The strings are put into $auth1, $auth2, etc, +up to a maximum. To retain backwards compatibility, they are also put int $1, +$2, etc. If the data consists of the string "=" it indicates a single, empty +string. */ + +int +auth_read_input(const uschar * data) +{ +if (Ustrcmp(data, "=") == 0) + { + auth_vars[0] = expand_nstring[++expand_nmax] = US""; + expand_nlength[expand_nmax] = 0; + } +else + { + uschar * clear, * end; + int len; + + if ((len = b64decode(data, &clear)) < 0) return BAD64; + DEBUG(D_auth) debug_printf("auth input decode:"); + for (end = clear + len; clear < end && expand_nmax < EXPAND_MAXN; ) + { + DEBUG(D_auth) debug_printf(" '%s'", clear); + if (expand_nmax < AUTH_VARS) auth_vars[expand_nmax] = clear; + expand_nstring[++expand_nmax] = clear; + while (*clear != 0) clear++; + expand_nlength[expand_nmax] = clear++ - expand_nstring[expand_nmax]; + } + DEBUG(D_auth) debug_printf("\n"); + } +return OK; +} + + + + +/************************************************* +* Issue a challenge and get a response * +*************************************************/ + +/* This function is used by authentication drivers to b64-encode and +output a challenge to the SMTP client, and read the response line. + +Arguments: + aptr set to point to the response (which is in big_buffer) + challenge the challenge data (unencoded, may be binary) + challen the length of the challenge data, in bytes + +Returns: OK on success + BAD64 if response too large for buffer + CANCELLED if response is "*" +*/ + +int +auth_get_data(uschar ** aptr, const uschar * challenge, int challen) +{ +int c; +int p = 0; +smtp_printf("334 %s\r\n", FALSE, b64encode(challenge, challen)); +while ((c = receive_getc(GETC_BUFFER_UNLIMITED)) != '\n' && c != EOF) + { + if (p >= big_buffer_size - 1) return BAD64; + big_buffer[p++] = c; + } +if (p > 0 && big_buffer[p-1] == '\r') p--; +big_buffer[p] = 0; +DEBUG(D_receive) debug_printf("SMTP<< %s\n", big_buffer); +if (Ustrcmp(big_buffer, "*") == 0) return CANCELLED; +*aptr = big_buffer; +return OK; +} + + + +int +auth_prompt(const uschar * challenge) +{ +int rc, len; +uschar * resp, * clear, * end; + +if ((rc = auth_get_data(&resp, challenge, Ustrlen(challenge))) != OK) + return rc; +if ((len = b64decode(resp, &clear)) < 0) + return BAD64; +end = clear + len; + +/* This loop must run at least once, in case the length is zero */ +do + { + if (expand_nmax < AUTH_VARS) auth_vars[expand_nmax] = clear; + expand_nstring[++expand_nmax] = clear; + while (*clear != 0) clear++; + expand_nlength[expand_nmax] = clear++ - expand_nstring[expand_nmax]; + } +while (clear < end && expand_nmax < EXPAND_MAXN); +return OK; +} + + +/*********************************************** +* Send an AUTH-negotiation item * +************************************************/ + +/* Expand and send one client auth item and read the response. +Include the AUTH command and method if tagged as "first". Use the given buffer +for receiving the b6-encoded reply; decode it it return it in the string arg. + +Return: + OK success + FAIL_SEND error after writing a command; errno is set + FAIL failed after reading a response; + either errno is set (for timeouts, I/O failures) or + the buffer contains the SMTP response line + CANCELLED the client cancelled authentication (often "fail" in expansion) + the buffer may contain a message; if not, *buffer = 0 + ERROR local problem (typically expansion error); message in buffer + DEFER more items expected +*/ + +int +auth_client_item(void * sx, auth_instance * ablock, const uschar ** inout, + unsigned flags, int timeout, uschar * buffer, int buffsize) +{ +int len, clear_len; +uschar * ss, * clear; + +ss = US expand_cstring(*inout); +if (ss == *inout) ss = string_copy(ss); + +/* Forced expansion failure is not an error; authentication is abandoned. On +all but the first string, we have to abandon the authentication attempt by +sending a line containing "*". Save the failed expansion string, because it +is in big_buffer, and that gets used by the sending function. */ + +if (!ss) + { + if (!(flags & AUTH_ITEM_FIRST)) + { + if (smtp_write_command(sx, SCMD_FLUSH, "*\r\n") >= 0) + (void) smtp_read_response(sx, US buffer, buffsize, '2', timeout); + } + if (f.expand_string_forcedfail) + { + *buffer = 0; /* No message */ + return CANCELLED; + } + string_format(buffer, buffsize, "expansion of \"%s\" failed in %s " + "authenticator: %s", *inout, ablock->name, expand_string_message); + return ERROR; + } + +len = Ustrlen(ss); + +/* The character ^ is used as an escape for a binary zero character, which is +needed for the PLAIN mechanism. It must be doubled if really needed. + +The parsing ambiguity of ^^^ is taken as ^^ -> ^ ; ^ -> NUL - and there is +no way to get a leading ^ after a NUL. We would need to intro new syntax to +support that (probably preferring to take a more-standard exim list as a source +and concat the elements with intervening NULs. Either a magic marker on the +source string for client_send, or a new option). */ + +for (int i = 0; i < len; i++) + if (ss[i] == '^') + if (ss[i+1] != '^') + ss[i] = 0; + else + if (--len > i+1) memmove(ss + i + 1, ss + i + 2, len - i); + +/* The first string is attached to the AUTH command; others are sent +unembellished. */ + +if (flags & AUTH_ITEM_FIRST) + { + if (smtp_write_command(sx, SCMD_FLUSH, "AUTH %s%s%s\r\n", + ablock->public_name, len == 0 ? "" : " ", b64encode(CUS ss, len)) < 0) + return FAIL_SEND; + } +else + if (smtp_write_command(sx, SCMD_FLUSH, "%s\r\n", b64encode(CUS ss, len)) < 0) + return FAIL_SEND; + +/* If we receive a success response from the server, authentication +has succeeded. There may be more data to send, but is there any point +in provoking an error here? */ + +if (smtp_read_response(sx, buffer, buffsize, '2', timeout)) + { + *inout = NULL; + return OK; + } + +/* Not a success response. If errno != 0 there is some kind of transmission +error. Otherwise, check the response code in the buffer. If it starts with +'3', more data is expected. */ + +if (errno != 0 || buffer[0] != '3') return FAIL; + +/* If there is no more data to send, we have to cancel the authentication +exchange and return ERROR. */ + +if (flags & AUTH_ITEM_LAST) + { + if (smtp_write_command(sx, SCMD_FLUSH, "*\r\n") >= 0) + (void)smtp_read_response(sx, US buffer, buffsize, '2', timeout); + string_format(buffer, buffsize, "Too few items in client_send in %s " + "authenticator", ablock->name); + return ERROR; + } + +/* Now that we know we'll continue, we put the received data into $auth, +if possible. First, decode it: buffer+4 skips over the SMTP status code. */ + +clear_len = b64decode(buffer+4, &clear); + +/* If decoding failed, the default is to terminate the authentication, and +return FAIL, with the SMTP response still in the buffer. However, if client_ +ignore_invalid_base64 is set, we ignore the error, and put an empty string +into $auth. */ + +if (clear_len < 0) + { + uschar *save_bad = string_copy(buffer); + if (!(flags & AUTH_ITEM_IGN64)) + { + if (smtp_write_command(sx, SCMD_FLUSH, "*\r\n") >= 0) + (void)smtp_read_response(sx, US buffer, buffsize, '2', timeout); + string_format(buffer, buffsize, "Invalid base64 string in server " + "response \"%s\"", save_bad); + return CANCELLED; + } + DEBUG(D_auth) debug_printf("bad b64 decode for '%s';" + " ignoring due to client_ignore_invalid_base64\n", save_bad); + clear = string_copy(US""); + clear_len = 0; + } + +*inout = clear; +return DEFER; +} + + +/* End of get_data.c */ diff --git a/src/auths/get_no64_data.c b/src/auths/get_no64_data.c new file mode 100644 index 0000000..a019756 --- /dev/null +++ b/src/auths/get_no64_data.c @@ -0,0 +1,47 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" + + +/************************************************* +* Issue a non-b64 challenge and get a response * +*************************************************/ + +/* This function is used by authentication drivers to output a challenge +to the SMTP client and read the response line. This version does not use base +64 encoding for the text on the 334 line. It is used by the SPA, dovecot +and gsasl authenticators. + +Arguments: + aptr set to point to the response (which is in big_buffer) + challenge the challenge text (unencoded) + +Returns: OK on success + BAD64 if response too large for buffer + CANCELLED if response is "*" +*/ + +int +auth_get_no64_data(uschar **aptr, uschar *challenge) +{ +int c; +int p = 0; +smtp_printf("334 %s\r\n", FALSE, challenge); +while ((c = receive_getc(GETC_BUFFER_UNLIMITED)) != '\n' && c != EOF) + { + if (p >= big_buffer_size - 1) return BAD64; + big_buffer[p++] = c; + } +if (p > 0 && big_buffer[p-1] == '\r') p--; +big_buffer[p] = 0; +if (Ustrcmp(big_buffer, "*") == 0) return CANCELLED; +*aptr = big_buffer; +return OK; +} + +/* End of get_no64_data.c */ diff --git a/src/auths/gsasl_exim.c b/src/auths/gsasl_exim.c new file mode 100644 index 0000000..bae5f08 --- /dev/null +++ b/src/auths/gsasl_exim.c @@ -0,0 +1,1024 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2019 - 2022 */ +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Copyright (c) Twitter Inc 2012 + Author: Phil Pennock */ +/* Copyright (c) Phil Pennock 2012 */ + +/* Interface to GNU SASL library for generic authentication. */ + +/* Trade-offs: + +GNU SASL does not provide authentication data itself, so we have to expose +that decision to configuration. For some mechanisms, we need to act much +like plaintext. For others, we only need to be able to provide some +evaluated data on demand. There's no abstracted way (ie, without hardcoding +knowledge of authenticators here) to know which need what properties; we +can't query a session or the library for "we will need these for mechanism X". + +So: we always require server_condition, even if sometimes it will just be +set as "yes". We do provide a number of other hooks, which might not make +sense in all contexts. For some, we can do checks at init time. +*/ + +#include "../exim.h" + +#ifndef AUTH_GSASL +/* dummy function to satisfy compilers when we link in an "empty" file. */ +static void dummy(int x); +static void dummy2(int x) { dummy(x-1); } +static void dummy(int x) { dummy2(x-1); } +#else + +#include +#include "gsasl_exim.h" + + +#if GSASL_VERSION_MINOR >= 10 +# define EXIM_GSASL_HAVE_SCRAM_SHA_256 +# define EXIM_GSASL_SCRAM_S_KEY + +#elif GSASL_VERSION_MINOR == 9 +# define EXIM_GSASL_HAVE_SCRAM_SHA_256 + +# if GSASL_VERSION_PATCH >= 1 +# define EXIM_GSASL_SCRAM_S_KEY +# endif +# if GSASL_VERSION_PATCH < 2 +# define CHANNELBIND_HACK +# endif + +#else +# define CHANNELBIND_HACK +#endif + +/* Convenience for testing strings */ + +#define STREQIC(Foo, Bar) (strcmpic((Foo), (Bar)) == 0) + + +/* Authenticator-specific options. */ +/* I did have server_*_condition options for various mechanisms, but since +we only ever handle one mechanism at a time, I didn't see the point in keeping +that. In case someone sees a point, I've left the condition_check() API +alone. */ +#define LOFF(field) OPT_OFF(auth_gsasl_options_block, field) + +optionlist auth_gsasl_options[] = { + { "client_authz", opt_stringptr, LOFF(client_authz) }, + { "client_channelbinding", opt_bool, LOFF(client_channelbinding) }, + { "client_password", opt_stringptr, LOFF(client_password) }, + { "client_spassword", opt_stringptr, LOFF(client_spassword) }, + { "client_username", opt_stringptr, LOFF(client_username) }, + + { "server_channelbinding", opt_bool, LOFF(server_channelbinding) }, + { "server_hostname", opt_stringptr, LOFF(server_hostname) }, +#ifdef EXIM_GSASL_SCRAM_S_KEY + { "server_key", opt_stringptr, LOFF(server_key) }, +#endif + { "server_mech", opt_stringptr, LOFF(server_mech) }, + { "server_password", opt_stringptr, LOFF(server_password) }, + { "server_realm", opt_stringptr, LOFF(server_realm) }, + { "server_scram_iter", opt_stringptr, LOFF(server_scram_iter) }, + { "server_scram_salt", opt_stringptr, LOFF(server_scram_salt) }, +#ifdef EXIM_GSASL_SCRAM_S_KEY + { "server_skey", opt_stringptr, LOFF(server_s_key) }, +#endif + { "server_service", opt_stringptr, LOFF(server_service) } +}; + +int auth_gsasl_options_count = + sizeof(auth_gsasl_options)/sizeof(optionlist); + +/* Defaults for the authenticator-specific options. */ +auth_gsasl_options_block auth_gsasl_option_defaults = { + .server_service = US"smtp", + .server_hostname = US"$primary_hostname", + .server_scram_iter = US"4096", + /* all others zero/null */ +}; + + +#ifdef MACRO_PREDEF +# include "../macro_predef.h" + +/* Dummy values */ +void auth_gsasl_init(auth_instance *ablock) {} +int auth_gsasl_server(auth_instance *ablock, uschar *data) {return 0;} +int auth_gsasl_client(auth_instance *ablock, void * sx, + int timeout, uschar *buffer, int buffsize) {return 0;} +gstring * auth_gsasl_version_report(gstring * g) {return NULL;} + +void +auth_gsasl_macros(void) +{ +# ifdef EXIM_GSASL_HAVE_SCRAM_SHA_256 + builtin_macro_create(US"_HAVE_AUTH_GSASL_SCRAM_SHA_256"); +# endif +# ifdef EXIM_GSASL_SCRAM_S_KEY + builtin_macro_create(US"_HAVE_AUTH_GSASL_SCRAM_S_KEY"); +# endif +} + +#else /*!MACRO_PREDEF*/ + + + +/* "Globals" for managing the gsasl interface. */ + +static Gsasl *gsasl_ctx = NULL; +static int + main_callback(Gsasl *ctx, Gsasl_session *sctx, Gsasl_property prop); +static int + server_callback(Gsasl *ctx, Gsasl_session *sctx, Gsasl_property prop, auth_instance *ablock); +static int + client_callback(Gsasl *ctx, Gsasl_session *sctx, Gsasl_property prop, auth_instance *ablock); + +static BOOL sasl_error_should_defer = FALSE; +static Gsasl_property callback_loop = 0; +static BOOL checked_server_condition = FALSE; + +enum { CURRENTLY_SERVER = 1, CURRENTLY_CLIENT = 2 }; + +struct callback_exim_state { + auth_instance *ablock; + int currently; +}; + + +/************************************************* +* Initialization entry point * +*************************************************/ + +/* Called for each instance, after its options have been read, to +enable consistency checks to be done, or anything else that needs +to be set up. */ + +void +auth_gsasl_init(auth_instance *ablock) +{ +static char * once = NULL; +int rc; +auth_gsasl_options_block *ob = + (auth_gsasl_options_block *)(ablock->options_block); + +/* As per existing Cyrus glue, use the authenticator's public name as +the default for the mechanism name; we don't handle multiple mechanisms +in one authenticator, but the same driver can be used multiple times. */ + +if (!ob->server_mech) + ob->server_mech = string_copy(ablock->public_name); + +/* Can get multiple session contexts from one library context, so just +initialise the once. */ + +if (!gsasl_ctx) + { + if ((rc = gsasl_init(&gsasl_ctx)) != GSASL_OK) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s authenticator: " + "couldn't initialise GNU SASL library: %s (%s)", + ablock->name, gsasl_strerror_name(rc), gsasl_strerror(rc)); + + gsasl_callback_set(gsasl_ctx, main_callback); + } + +/* We don't need this except to log it for debugging. */ + +HDEBUG(D_auth) if (!once) + { + if ((rc = gsasl_server_mechlist(gsasl_ctx, &once)) != GSASL_OK) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s authenticator: " + "failed to retrieve list of mechanisms: %s (%s)", + ablock->name, gsasl_strerror_name(rc), gsasl_strerror(rc)); + + debug_printf("GNU SASL supports: %s\n", once); + } + +if (!gsasl_client_support_p(gsasl_ctx, CCS ob->server_mech)) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s authenticator: " + "GNU SASL does not support mechanism \"%s\"", + ablock->name, ob->server_mech); + +if (ablock->server_condition) + ablock->server = TRUE; +else if( ob->server_mech + && !STREQIC(ob->server_mech, US"EXTERNAL") + && !STREQIC(ob->server_mech, US"ANONYMOUS") + && !STREQIC(ob->server_mech, US"PLAIN") + && !STREQIC(ob->server_mech, US"LOGIN") + ) + { + /* At present, for mechanisms we don't panic on absence of server_condition; + need to figure out the most generically correct approach to deciding when + it's critical and when it isn't. Eg, for simple validation (PLAIN mechanism, + etc) it clearly is critical. + */ + + ablock->server = FALSE; + HDEBUG(D_auth) debug_printf("%s authenticator: " + "Need server_condition for %s mechanism\n", + ablock->name, ob->server_mech); + } + +/* This does *not* scale to new SASL mechanisms. Need a better way to ask +which properties will be needed. */ + +if ( !ob->server_realm + && STREQIC(ob->server_mech, US"DIGEST-MD5")) + { + ablock->server = FALSE; + HDEBUG(D_auth) debug_printf("%s authenticator: " + "Need server_realm for %s mechanism\n", + ablock->name, ob->server_mech); + } + +ablock->client = ob->client_username && ob->client_password; +} + + +/* GNU SASL uses one top-level callback, registered at library level. +We dispatch to client and server functions instead. */ + +static int +main_callback(Gsasl *ctx, Gsasl_session *sctx, Gsasl_property prop) +{ +int rc = 0; +struct callback_exim_state *cb_state = + (struct callback_exim_state *)gsasl_session_hook_get(sctx); + +if (!cb_state) + { + HDEBUG(D_auth) debug_printf("gsasl callback (%d) not from our server/client processing\n", prop); +#ifdef CHANNELBIND_HACK + if (prop == GSASL_CB_TLS_UNIQUE) + { + uschar * s; + if ((s = gsasl_callback_hook_get(ctx))) + { + HDEBUG(D_auth) debug_printf("GSASL_CB_TLS_UNIQUE from ctx hook\n"); + gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE, CS s); + } + else + { + HDEBUG(D_auth) debug_printf("GSASL_CB_TLS_UNIQUE! dummy for now\n"); + gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE, ""); + } + return GSASL_OK; + } +#endif + return GSASL_NO_CALLBACK; + } + +HDEBUG(D_auth) + debug_printf("GNU SASL Callback entered, prop=%d (loop prop=%d)\n", + prop, callback_loop); + +if (callback_loop > 0) + { + /* Most likely is that we were asked for property FOO, and to + expand the string we asked for property BAR to put into an auth + variable, but property BAR is not supplied for this mechanism. */ + HDEBUG(D_auth) + debug_printf("Loop, asked for property %d while handling property %d\n", + prop, callback_loop); + return GSASL_NO_CALLBACK; + } +callback_loop = prop; + +if (cb_state->currently == CURRENTLY_CLIENT) + rc = client_callback(ctx, sctx, prop, cb_state->ablock); +else if (cb_state->currently == CURRENTLY_SERVER) + rc = server_callback(ctx, sctx, prop, cb_state->ablock); +else + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s authenticator: " + "unhandled callback state, bug in Exim", cb_state->ablock->name); + /* NOTREACHED */ + +callback_loop = 0; +return rc; +} + + +/************************************************* +* Debug service function * +*************************************************/ +static const uschar * +gsasl_prop_code_to_name(Gsasl_property prop) +{ +switch (prop) + { + case GSASL_AUTHID: return US"AUTHID"; + case GSASL_AUTHZID: return US"AUTHZID"; + case GSASL_PASSWORD: return US"PASSWORD"; + case GSASL_ANONYMOUS_TOKEN: return US"ANONYMOUS_TOKEN"; + case GSASL_SERVICE: return US"SERVICE"; + case GSASL_HOSTNAME: return US"HOSTNAME"; + case GSASL_GSSAPI_DISPLAY_NAME: return US"GSSAPI_DISPLAY_NAME"; + case GSASL_PASSCODE: return US"PASSCODE"; + case GSASL_SUGGESTED_PIN: return US"SUGGESTED_PIN"; + case GSASL_PIN: return US"PIN"; + case GSASL_REALM: return US"REALM"; + case GSASL_DIGEST_MD5_HASHED_PASSWORD: return US"DIGEST_MD5_HASHED_PASSWORD"; + case GSASL_QOPS: return US"QOPS"; + case GSASL_QOP: return US"QOP"; + case GSASL_SCRAM_ITER: return US"SCRAM_ITER"; + case GSASL_SCRAM_SALT: return US"SCRAM_SALT"; + case GSASL_SCRAM_SALTED_PASSWORD: return US"SCRAM_SALTED_PASSWORD"; +#ifdef EXIM_GSASL_SCRAM_S_KEY + case GSASL_SCRAM_STOREDKEY: return US"SCRAM_STOREDKEY"; + case GSASL_SCRAM_SERVERKEY: return US"SCRAM_SERVERKEY"; +#endif + case GSASL_CB_TLS_UNIQUE: return US"CB_TLS_UNIQUE"; + case GSASL_SAML20_IDP_IDENTIFIER: return US"SAML20_IDP_IDENTIFIER"; + case GSASL_SAML20_REDIRECT_URL: return US"SAML20_REDIRECT_URL"; + case GSASL_OPENID20_REDIRECT_URL: return US"OPENID20_REDIRECT_URL"; + case GSASL_OPENID20_OUTCOME_DATA: return US"OPENID20_OUTCOME_DATA"; + case GSASL_SAML20_AUTHENTICATE_IN_BROWSER: return US"SAML20_AUTHENTICATE_IN_BROWSER"; + case GSASL_OPENID20_AUTHENTICATE_IN_BROWSER: return US"OPENID20_AUTHENTICATE_IN_BROWSER"; + case GSASL_VALIDATE_SIMPLE: return US"VALIDATE_SIMPLE"; + case GSASL_VALIDATE_EXTERNAL: return US"VALIDATE_EXTERNAL"; + case GSASL_VALIDATE_ANONYMOUS: return US"VALIDATE_ANONYMOUS"; + case GSASL_VALIDATE_GSSAPI: return US"VALIDATE_GSSAPI"; + case GSASL_VALIDATE_SECURID: return US"VALIDATE_SECURID"; + case GSASL_VALIDATE_SAML20: return US"VALIDATE_SAML20"; + case GSASL_VALIDATE_OPENID20: return US"VALIDATE_OPENID20"; + } +return CUS string_sprintf("(unknown prop: %d)", (int)prop); +} + +/************************************************* +* Server entry point * +*************************************************/ + +/* For interface, see auths/README */ + +int +auth_gsasl_server(auth_instance *ablock, uschar *initial_data) +{ +char *tmps; +char *to_send, *received; +Gsasl_session *sctx = NULL; +auth_gsasl_options_block *ob = + (auth_gsasl_options_block *)(ablock->options_block); +struct callback_exim_state cb_state; +int rc, auth_result, exim_error, exim_error_override; + +HDEBUG(D_auth) + debug_printf("GNU SASL: initialising session for %s, mechanism %s\n", + ablock->name, ob->server_mech); + +#ifndef DISABLE_TLS +if (tls_in.channelbinding && ob->server_channelbinding) + { +# ifndef DISABLE_TLS_RESUME + if (!tls_in.ext_master_secret && tls_in.resumption == RESUME_USED) + { /* per RFC 7677 section 4 */ + HDEBUG(D_auth) debug_printf( + "channel binding not usable on resumed TLS without extended-master-secret"); + return FAIL; + } +# endif +# ifdef CHANNELBIND_HACK +/* This is a gross hack to get around the library before 1.9.2 +a) requiring that c-b was already set, at the _start() call, and +b) caching a b64'd version of the binding then which it never updates. */ + + gsasl_callback_hook_set(gsasl_ctx, tls_in.channelbinding); +# endif + } +#endif + +if ((rc = gsasl_server_start(gsasl_ctx, CCS ob->server_mech, &sctx)) != GSASL_OK) + { + auth_defer_msg = string_sprintf("GNU SASL: session start failure: %s (%s)", + gsasl_strerror_name(rc), gsasl_strerror(rc)); + HDEBUG(D_auth) debug_printf("%s\n", auth_defer_msg); + return DEFER; + } +/* Hereafter: gsasl_finish(sctx) please */ + +cb_state.ablock = ablock; +cb_state.currently = CURRENTLY_SERVER; +gsasl_session_hook_set(sctx, &cb_state); + +tmps = CS expand_string(ob->server_service); +gsasl_property_set(sctx, GSASL_SERVICE, tmps); +tmps = CS expand_string(ob->server_hostname); +gsasl_property_set(sctx, GSASL_HOSTNAME, tmps); +if (ob->server_realm) + { + tmps = CS expand_string(ob->server_realm); + if (tmps && *tmps) + gsasl_property_set(sctx, GSASL_REALM, tmps); + } +/* We don't support protection layers. */ +gsasl_property_set(sctx, GSASL_QOPS, "qop-auth"); + +#ifndef DISABLE_TLS +if (tls_in.channelbinding) + { + /* Some auth mechanisms can ensure that both sides are talking withing the + same security context; for TLS, this means that even if a bad certificate + has been accepted, they remain MitM-proof because both sides must be within + the same negotiated session; if someone is terminating one session and + proxying data on within a second, authentication will fail. + + We might not have this available, depending upon TLS implementation, + ciphersuite, phase of moon ... + + If we do, it results in extra SASL mechanisms being available; here, + Exim's one-mechanism-per-authenticator potentially causes problems. + It depends upon how GNU SASL will implement the PLUS variants of GS2 + and whether it automatically mandates a switch to the bound PLUS + if the data is available. Since default-on, despite being more secure, + would then result in mechanism name changes on a library update, we + have little choice but to default it off and let the admin choose to + enable it. *sigh* + + Earlier library versions need this set early, during the _start() call, + so we had to misuse gsasl_callback_hook_set/get() as a data transfer + mech for the callback done at that time to get the bind-data. More recently + the callback is done (if needed) during the first gsasl_stop(). We know + the bind-data here so can set it (and should not get a callback). + */ + if (ob->server_channelbinding) + { + HDEBUG(D_auth) debug_printf("Auth %s: Enabling channel-binding\n", + ablock->name); +# ifndef CHANNELBIND_HACK + gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE, CCS tls_in.channelbinding); +# endif + } + else + HDEBUG(D_auth) + debug_printf("Auth %s: Not enabling channel-binding (data available)\n", + ablock->name); + } +else + HDEBUG(D_auth) + debug_printf("Auth %s: no channel-binding data available\n", + ablock->name); +#endif + +checked_server_condition = FALSE; + +received = CS initial_data; +to_send = NULL; +exim_error = exim_error_override = OK; + +do { + switch (rc = gsasl_step64(sctx, received, &to_send)) + { + case GSASL_OK: + if (!to_send) + goto STOP_INTERACTION; + break; + + case GSASL_NEEDS_MORE: + break; + + case GSASL_AUTHENTICATION_ERROR: + case GSASL_INTEGRITY_ERROR: + case GSASL_NO_AUTHID: + case GSASL_NO_ANONYMOUS_TOKEN: + case GSASL_NO_AUTHZID: + case GSASL_NO_PASSWORD: + case GSASL_NO_PASSCODE: + case GSASL_NO_PIN: + case GSASL_BASE64_ERROR: + HDEBUG(D_auth) debug_printf("GNU SASL permanent error: %s (%s)\n", + gsasl_strerror_name(rc), gsasl_strerror(rc)); + log_write(0, LOG_REJECT, "%s authenticator (%s):\n " + "GNU SASL permanent failure: %s (%s)", + ablock->name, ob->server_mech, + gsasl_strerror_name(rc), gsasl_strerror(rc)); + if (rc == GSASL_BASE64_ERROR) + exim_error_override = BAD64; + goto STOP_INTERACTION; + + default: + auth_defer_msg = string_sprintf("GNU SASL temporary error: %s (%s)", + gsasl_strerror_name(rc), gsasl_strerror(rc)); + HDEBUG(D_auth) debug_printf("%s\n", auth_defer_msg); + exim_error_override = DEFER; + goto STOP_INTERACTION; + } + + /*XXX having our caller send the final smtp "235" is unfortunate; wastes a roundtrip */ + if ((rc == GSASL_NEEDS_MORE) || (to_send && *to_send)) + exim_error = auth_get_no64_data(USS &received, US to_send); + + if (to_send) + { + free(to_send); + to_send = NULL; + } + + if (exim_error) + break; /* handles * cancelled check */ + + } while (rc == GSASL_NEEDS_MORE); + +STOP_INTERACTION: +auth_result = rc; + +HDEBUG(D_auth) + { + const uschar * s; + if ((s = CUS gsasl_property_fast(sctx, GSASL_SCRAM_ITER))) + debug_printf(" - itercnt: '%s'\n", s); + if ((s = CUS gsasl_property_fast(sctx, GSASL_SCRAM_SALT))) + debug_printf(" - salt: '%s'\n", s); +#ifdef EXIM_GSASL_SCRAM_S_KEY + if ((s = CUS gsasl_property_fast(sctx, GSASL_SCRAM_SERVERKEY))) + debug_printf(" - ServerKey: '%s'\n", s); + if ((s = CUS gsasl_property_fast(sctx, GSASL_SCRAM_STOREDKEY))) + debug_printf(" - StoredKey: '%s'\n", s); +#endif + } + +gsasl_finish(sctx); + +/* Can return: OK DEFER FAIL CANCELLED BAD64 UNEXPECTED */ + +if (exim_error != OK) + return exim_error; + +if (auth_result != GSASL_OK) + { + HDEBUG(D_auth) debug_printf("authentication returned %s (%s)\n", + gsasl_strerror_name(auth_result), gsasl_strerror(auth_result)); + if (exim_error_override != OK) + return exim_error_override; /* might be DEFER */ + if (sasl_error_should_defer) /* overriding auth failure SASL error */ + return DEFER; + return FAIL; + } + +/* Auth succeeded, check server_condition unless already done in callback */ +return checked_server_condition ? OK : auth_check_serv_cond(ablock); +} + + +/* returns the GSASL status of expanding the Exim string given */ +static int +condition_check(auth_instance *ablock, uschar *label, uschar *condition_string) +{ +int exim_rc = auth_check_some_cond(ablock, label, condition_string, FAIL); +switch (exim_rc) + { + case OK: return GSASL_OK; + case DEFER: sasl_error_should_defer = TRUE; + return GSASL_AUTHENTICATION_ERROR; + case FAIL: return GSASL_AUTHENTICATION_ERROR; + default: log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s authenticator: " + "Unhandled return from checking %s: %d", + ablock->name, label, exim_rc); + } + +/* NOTREACHED */ +return GSASL_AUTHENTICATION_ERROR; +} + + +/* Set the "next" $auth[n] and increment expand_nmax */ + +static void +set_exim_authvar_from_prop(Gsasl_session * sctx, Gsasl_property prop) +{ +uschar * propval = US gsasl_property_fast(sctx, prop); +int i = expand_nmax, j = i + 1; +propval = propval ? string_copy(propval) : US""; +HDEBUG(D_auth) debug_printf("auth[%d] <= %s'%s'\n", + j, gsasl_prop_code_to_name(prop), propval); +expand_nstring[j] = propval; +expand_nlength[j] = Ustrlen(propval); +if (i < AUTH_VARS) auth_vars[i] = propval; +expand_nmax = j; +} + +static void +set_exim_authvars_from_a_az_r_props(Gsasl_session * sctx) +{ +if (expand_nmax > 0 ) return; + +/* Asking for GSASL_AUTHZID calls back into us if we use +gsasl_property_get(), thus the use of gsasl_property_fast(). +Do we really want to hardcode limits per mechanism? What happens when +a new mechanism is added to the library. It *shouldn't* result in us +needing to add more glue, since avoiding that is a large part of the +point of SASL. */ + +set_exim_authvar_from_prop(sctx, GSASL_AUTHID); +set_exim_authvar_from_prop(sctx, GSASL_AUTHZID); +set_exim_authvar_from_prop(sctx, GSASL_REALM); +} + + +static int +prop_from_option(Gsasl_session * sctx, Gsasl_property prop, + const uschar * option) +{ +HDEBUG(D_auth) debug_printf(" %s\n", gsasl_prop_code_to_name(prop)); +if (option) + { + set_exim_authvars_from_a_az_r_props(sctx); + option = expand_cstring(option); + HDEBUG(D_auth) debug_printf(" '%s'\n", option); + if (*option) + gsasl_property_set(sctx, prop, CCS option); + return GSASL_OK; + } +HDEBUG(D_auth) debug_printf(" option not set\n"); +return GSASL_NO_CALLBACK; +} + +static int +server_callback(Gsasl *ctx, Gsasl_session *sctx, Gsasl_property prop, + auth_instance *ablock) +{ +char * tmps; +uschar * s; +int cbrc = GSASL_NO_CALLBACK; +auth_gsasl_options_block * ob = + (auth_gsasl_options_block *)(ablock->options_block); + +HDEBUG(D_auth) debug_printf("GNU SASL callback %s for %s/%s as server\n", + gsasl_prop_code_to_name(prop), ablock->name, ablock->public_name); + +for (int i = 0; i < AUTH_VARS; i++) auth_vars[i] = NULL; +expand_nmax = 0; + +switch (prop) + { + case GSASL_VALIDATE_SIMPLE: + /* GSASL_AUTHID, GSASL_AUTHZID, and GSASL_PASSWORD */ + set_exim_authvar_from_prop(sctx, GSASL_AUTHID); + set_exim_authvar_from_prop(sctx, GSASL_AUTHZID); + set_exim_authvar_from_prop(sctx, GSASL_PASSWORD); + + cbrc = condition_check(ablock, US"server_condition", ablock->server_condition); + checked_server_condition = TRUE; + break; + + case GSASL_VALIDATE_EXTERNAL: + if (!ablock->server_condition) + { + HDEBUG(D_auth) debug_printf("No server_condition supplied, to validate EXTERNAL\n"); + cbrc = GSASL_AUTHENTICATION_ERROR; + break; + } + set_exim_authvar_from_prop(sctx, GSASL_AUTHZID); + + cbrc = condition_check(ablock, + US"server_condition (EXTERNAL)", ablock->server_condition); + checked_server_condition = TRUE; + break; + + case GSASL_VALIDATE_ANONYMOUS: + if (!ablock->server_condition) + { + HDEBUG(D_auth) debug_printf("No server_condition supplied, to validate ANONYMOUS\n"); + cbrc = GSASL_AUTHENTICATION_ERROR; + break; + } + set_exim_authvar_from_prop(sctx, GSASL_ANONYMOUS_TOKEN); + + cbrc = condition_check(ablock, + US"server_condition (ANONYMOUS)", ablock->server_condition); + checked_server_condition = TRUE; + break; + + case GSASL_VALIDATE_GSSAPI: + /* GSASL_AUTHZID and GSASL_GSSAPI_DISPLAY_NAME + The display-name is authenticated as part of GSS, the authzid is claimed + by the SASL integration after authentication; protected against tampering + (if the SASL mechanism supports that, which Kerberos does) but is + unverified, same as normal for other mechanisms. + First coding, we had these values swapped, but for consistency and prior + to the first release of Exim with this authenticator, they've been + switched to match the ordering of GSASL_VALIDATE_SIMPLE. */ + + set_exim_authvar_from_prop(sctx, GSASL_GSSAPI_DISPLAY_NAME); + set_exim_authvar_from_prop(sctx, GSASL_AUTHZID); + + /* In this one case, it perhaps makes sense to default back open? + But for consistency, let's just mandate server_condition here too. */ + + cbrc = condition_check(ablock, + US"server_condition (GSSAPI family)", ablock->server_condition); + checked_server_condition = TRUE; + break; + + case GSASL_SCRAM_ITER: + cbrc = prop_from_option(sctx, prop, ob->server_scram_iter); + break; + + case GSASL_SCRAM_SALT: + cbrc = prop_from_option(sctx, prop, ob->server_scram_salt); + break; + +#ifdef EXIM_GSASL_SCRAM_S_KEY + case GSASL_SCRAM_STOREDKEY: + cbrc = prop_from_option(sctx, prop, ob->server_s_key); + break; + + case GSASL_SCRAM_SERVERKEY: + cbrc = prop_from_option(sctx, prop, ob->server_key); + break; +#endif + + case GSASL_PASSWORD: + /* SCRAM-*: GSASL_AUTHID, GSASL_AUTHZID and GSASL_REALM + DIGEST-MD5: GSASL_AUTHID, GSASL_AUTHZID and GSASL_REALM + CRAM-MD5: GSASL_AUTHID + PLAIN: GSASL_AUTHID and GSASL_AUTHZID + LOGIN: GSASL_AUTHID + */ + set_exim_authvars_from_a_az_r_props(sctx); + + if (!(s = ob->server_password)) + { + HDEBUG(D_auth) debug_printf("option not set\n"); + break; + } + if (!(tmps = CS expand_string(s))) + { + sasl_error_should_defer = !f.expand_string_forcedfail; + HDEBUG(D_auth) debug_printf("server_password expansion failed, so " + "can't tell GNU SASL library the password for %s\n", auth_vars[0]); + return GSASL_AUTHENTICATION_ERROR; + } + HDEBUG(D_auth) debug_printf(" set\n"); + gsasl_property_set(sctx, GSASL_PASSWORD, tmps); + + /* This is inadequate; don't think Exim's store stacks are geared + for memory wiping, so expanding strings will leave stuff laying around. + But no need to compound the problem, so get rid of the one we can. */ + + if (US tmps != s) memset(tmps, '\0', strlen(tmps)); + cbrc = GSASL_OK; + break; + + default: + HDEBUG(D_auth) debug_printf(" Unrecognised callback: %d\n", prop); + cbrc = GSASL_NO_CALLBACK; + } + +HDEBUG(D_auth) debug_printf("Returning %s (%s)\n", + gsasl_strerror_name(cbrc), gsasl_strerror(cbrc)); + +return cbrc; +} + + +/******************************************************************************/ + +#define PROP_OPTIONAL BIT(0) + +static BOOL +set_client_prop(Gsasl_session * sctx, Gsasl_property prop, uschar * val, + unsigned flags, uschar * buffer, int buffsize) +{ +uschar * s; + +if (!val) return !!(flags & PROP_OPTIONAL); +if (!(s = expand_string(val)) || !(flags & PROP_OPTIONAL) && !*s) + { + string_format(buffer, buffsize, "%s", expand_string_message); + return FALSE; + } +if (*s) + { + HDEBUG(D_auth) debug_printf("%s: set %s = '%s'\n", __FUNCTION__, + gsasl_prop_code_to_name(prop), s); + gsasl_property_set(sctx, prop, CS s); + } + +return TRUE; +} + +/************************************************* +* Client entry point * +*************************************************/ + +/* For interface, see auths/README */ + +int +auth_gsasl_client( + auth_instance *ablock, /* authenticator block */ + void * sx, /* connection */ + int timeout, /* command timeout */ + uschar *buffer, /* buffer for reading response */ + int buffsize) /* size of buffer */ +{ +auth_gsasl_options_block *ob = + (auth_gsasl_options_block *)(ablock->options_block); +Gsasl_session * sctx = NULL; +struct callback_exim_state cb_state; +uschar * s; +BOOL initial = TRUE; +int rc, yield = FAIL; + +HDEBUG(D_auth) + debug_printf("GNU SASL: initialising session for %s, mechanism %s\n", + ablock->name, ob->server_mech); + +*buffer = 0; + +#ifndef DISABLE_TLS +if (tls_out.channelbinding && ob->client_channelbinding) + { +# ifndef DISABLE_TLS_RESUME + if (!tls_out.ext_master_secret && tls_out.resumption == RESUME_USED) + { /* Per RFC 7677 section 4. See also RFC 7627, "Triple Handshake" + vulnerability, and https://www.mitls.org/pages/attacks/3SHAKE */ + string_format(buffer, buffsize, "%s", + "channel binding not usable on resumed TLS without extended-master-secret"); + return FAIL; + } +# endif +# ifdef CHANNELBIND_HACK + /* This is a gross hack to get around the library before 1.9.2 + a) requiring that c-b was already set, at the _start() call, and + b) caching a b64'd version of the binding then which it never updates. */ + + gsasl_callback_hook_set(gsasl_ctx, tls_out.channelbinding); +# endif + } +#endif + +if ((rc = gsasl_client_start(gsasl_ctx, CCS ob->server_mech, &sctx)) != GSASL_OK) + { + string_format(buffer, buffsize, "GNU SASL: session start failure: %s (%s)", + gsasl_strerror_name(rc), gsasl_strerror(rc)); + HDEBUG(D_auth) debug_printf("%s\n", buffer); + return ERROR; + } + +cb_state.ablock = ablock; +cb_state.currently = CURRENTLY_CLIENT; +gsasl_session_hook_set(sctx, &cb_state); + +/* Set properties */ + +if ( !set_client_prop(sctx, GSASL_PASSWORD, ob->client_password, + 0, buffer, buffsize) + || !set_client_prop(sctx, GSASL_AUTHID, ob->client_username, + 0, buffer, buffsize) + || !set_client_prop(sctx, GSASL_AUTHZID, ob->client_authz, + PROP_OPTIONAL, buffer, buffsize) + ) + return ERROR; + +#ifndef DISABLE_TLS +if (tls_out.channelbinding) + if (ob->client_channelbinding) + { + HDEBUG(D_auth) debug_printf("Auth %s: Enabling channel-binding\n", + ablock->name); +# ifndef CHANNELBIND_HACK + gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE, CCS tls_out.channelbinding); +# endif + } + else + HDEBUG(D_auth) + debug_printf("Auth %s: Not enabling channel-binding (data available)\n", + ablock->name); +#endif + +/* Run the SASL conversation with the server */ + +for(s = NULL; ;) + { + uschar * outstr; + BOOL fail = TRUE; + + rc = gsasl_step64(sctx, CS s, CSS &outstr); + + if (rc == GSASL_NEEDS_MORE || rc == GSASL_OK) + { + fail = initial + ? smtp_write_command(sx, SCMD_FLUSH, + outstr ? "AUTH %s %s\r\n" : "AUTH %s\r\n", + ablock->public_name, outstr) <= 0 + : outstr + ? smtp_write_command(sx, SCMD_FLUSH, "%s\r\n", outstr) <= 0 + : FALSE; + free(outstr); + if (fail) + { + yield = FAIL_SEND; + goto done; + } + initial = FALSE; + } + + if (rc != GSASL_NEEDS_MORE) + { + if (rc != GSASL_OK) + { + string_format(buffer, buffsize, "gsasl: %s", gsasl_strerror(rc)); + break; + } + + /* expecting a final 2xx from the server, accepting the AUTH */ + + if (smtp_read_response(sx, buffer, buffsize, '2', timeout)) + yield = OK; + break; /* from SASL sequence loop */ + } + + /* 2xx or 3xx response is acceptable. If 2xx, no further input */ + + if (!smtp_read_response(sx, buffer, buffsize, '3', timeout)) + if (errno == 0 && buffer[0] == '2') + buffer[4] = '\0'; + else + { + yield = FAIL; + goto done; + } + s = buffer + 4; + } + +done: +if (yield == OK) + { + expand_nmax = 0; + set_exim_authvar_from_prop(sctx, GSASL_AUTHID); + set_exim_authvar_from_prop(sctx, GSASL_SCRAM_ITER); + set_exim_authvar_from_prop(sctx, GSASL_SCRAM_SALT); + set_exim_authvar_from_prop(sctx, GSASL_SCRAM_SALTED_PASSWORD); + } + +gsasl_finish(sctx); +return yield; +} + +static int +client_callback(Gsasl *ctx, Gsasl_session *sctx, Gsasl_property prop, auth_instance *ablock) +{ +HDEBUG(D_auth) debug_printf("GNU SASL callback %s for %s/%s as client\n", + gsasl_prop_code_to_name(prop), ablock->name, ablock->public_name); +switch (prop) + { + case GSASL_CB_TLS_UNIQUE: /*XXX should never get called for this */ + HDEBUG(D_auth) + debug_printf(" filling in\n"); + gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE, CCS tls_out.channelbinding); + return GSASL_OK; + case GSASL_SCRAM_SALTED_PASSWORD: + { + uschar * client_spassword = + ((auth_gsasl_options_block *) ablock->options_block)->client_spassword; + uschar dummy[4]; + HDEBUG(D_auth) if (!client_spassword) + debug_printf(" client_spassword option unset\n"); + if (client_spassword) + { + expand_nmax = 0; + set_exim_authvar_from_prop(sctx, GSASL_AUTHID); + set_exim_authvar_from_prop(sctx, GSASL_SCRAM_ITER); + set_exim_authvar_from_prop(sctx, GSASL_SCRAM_SALT); + set_client_prop(sctx, GSASL_SCRAM_SALTED_PASSWORD, client_spassword, + 0, dummy, sizeof(dummy)); + for (int i = 0; i < AUTH_VARS; i++) auth_vars[i] = NULL; + expand_nmax = 0; + } + break; + } + default: + HDEBUG(D_auth) + debug_printf(" not providing one\n"); + break; + } +return GSASL_NO_CALLBACK; +} + +/************************************************* +* Diagnostic API * +*************************************************/ + +gstring * +auth_gsasl_version_report(gstring * g) +{ +return string_fmt_append(g, "Library version: GNU SASL: Compile: %s\n" + " Runtime: %s\n", + GSASL_VERSION, gsasl_check_version(NULL)); +} + + + +/* Dummy */ +void auth_gsasl_macros(void) {} + +#endif /*!MACRO_PREDEF*/ +#endif /* AUTH_GSASL */ + +/* End of gsasl_exim.c */ diff --git a/src/auths/gsasl_exim.h b/src/auths/gsasl_exim.h new file mode 100644 index 0000000..19c9036 --- /dev/null +++ b/src/auths/gsasl_exim.h @@ -0,0 +1,53 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2019 - 2022 */ +/* Copyright (c) University of Cambridge 1995 - 2012 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Copyright (c) Twitter Inc 2012 */ + +/* Interface to GNU SASL library for generic authentication. */ + +/* Authenticator-specific options. */ + +typedef struct { + uschar *server_service; + uschar *server_hostname; + uschar *server_realm; + uschar *server_mech; + uschar *server_password; + uschar *server_key; + uschar *server_s_key; + uschar *server_scram_iter; + uschar *server_scram_salt; + + uschar *client_username; + uschar *client_password; + uschar *client_authz; + uschar *client_spassword; + + BOOL server_channelbinding; + BOOL client_channelbinding; +} auth_gsasl_options_block; + +/* Data for reading the authenticator-specific options. */ + +extern optionlist auth_gsasl_options[]; +extern int auth_gsasl_options_count; + +/* Defaults for the authenticator-specific options. */ + +extern auth_gsasl_options_block auth_gsasl_option_defaults; + +/* The entry points for the mechanism */ + +extern void auth_gsasl_init(auth_instance *); +extern int auth_gsasl_server(auth_instance *, uschar *); +extern int auth_gsasl_client(auth_instance *, void *, + int, uschar *, int); +extern gstring * auth_gsasl_version_report(gstring *); +extern void auth_gsasl_macros(void); + +/* End of gsasl_exim.h */ diff --git a/src/auths/heimdal_gssapi.c b/src/auths/heimdal_gssapi.c new file mode 100644 index 0000000..3817632 --- /dev/null +++ b/src/auths/heimdal_gssapi.c @@ -0,0 +1,618 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Copyright (c) Twitter Inc 2012 + Author: Phil Pennock */ +/* Copyright (c) Phil Pennock 2012 */ + +/* Interface to Heimdal library for GSSAPI authentication. */ + +/* Naming and rationale + +Sensibly, this integration would be deferred to a SASL library, but none +of them appear to offer keytab file selection interfaces in their APIs. It +might be that this driver only requires minor modification to work with MIT +Kerberos. + +Heimdal provides a number of interfaces for various forms of authentication. +As GS2 does not appear to provide keytab control interfaces either, we may +end up supporting that too. It's possible that we could trivially expand to +support NTLM support via Heimdal, etc. Rather than try to be too generic +immediately, this driver is directly only supporting GSSAPI. + +Without rename, we could add an option for GS2 support in the future. +*/ + +/* Sources + +* mailcheck-imap (Perl, client-side, written by me years ago) +* gsasl driver (GPL, server-side) +* heimdal sources and man-pages, plus http://www.h5l.org/manual/ +* FreeBSD man-pages (very informative!) +* http://www.ggf.org/documents/GFD.24.pdf confirming GSS_KRB5_REGISTER_ACCEPTOR_IDENTITY_X + semantics, that found by browsing Heimdal source to find how to set the keytab; however, + after multiple attempts I failed to get that to work and instead switched to + gsskrb5_register_acceptor_identity(). +*/ + +#include "../exim.h" + +#ifndef AUTH_HEIMDAL_GSSAPI +/* dummy function to satisfy compilers when we link in an "empty" file. */ +static void dummy(int x); +static void dummy2(int x) { dummy(x-1); } +static void dummy(int x) { dummy2(x-1); } +#else + +#include +#include + +/* for the _init debugging */ +#include + +#include "heimdal_gssapi.h" + +/* Authenticator-specific options. */ +optionlist auth_heimdal_gssapi_options[] = { + { "server_hostname", opt_stringptr, + OPT_OFF(auth_heimdal_gssapi_options_block, server_hostname) }, + { "server_keytab", opt_stringptr, + OPT_OFF(auth_heimdal_gssapi_options_block, server_keytab) }, + { "server_service", opt_stringptr, + OPT_OFF(auth_heimdal_gssapi_options_block, server_service) } +}; + +int auth_heimdal_gssapi_options_count = + sizeof(auth_heimdal_gssapi_options)/sizeof(optionlist); + +/* Defaults for the authenticator-specific options. */ +auth_heimdal_gssapi_options_block auth_heimdal_gssapi_option_defaults = { + US"$primary_hostname", /* server_hostname */ + NULL, /* server_keytab */ + US"smtp", /* server_service */ +}; + + +#ifdef MACRO_PREDEF + +/* Dummy values */ +void auth_heimdal_gssapi_init(auth_instance *ablock) {} +int auth_heimdal_gssapi_server(auth_instance *ablock, uschar *data) {return 0;} +int auth_heimdal_gssapi_client(auth_instance *ablock, void * sx, + int timeout, uschar *buffer, int buffsize) {return 0;} +gstring * auth_heimdal_gssapi_version_report(gstring * g) {} + +#else /*!MACRO_PREDEF*/ + + + +/* "Globals" for managing the heimdal_gssapi interface. */ + +/* Utility functions */ +static void + exim_heimdal_error_debug(const char *, krb5_context, krb5_error_code); +static int + exim_gssapi_error_defer(rmark, OM_uint32, OM_uint32, const char *, ...) + PRINTF_FUNCTION(4, 5); + +#define EmptyBuf(buf) do { buf.value = NULL; buf.length = 0; } while (0) + + +/************************************************* +* Initialization entry point * +*************************************************/ + +/* Called for each instance, after its options have been read, to +enable consistency checks to be done, or anything else that needs +to be set up. */ + +/* Heimdal provides a GSSAPI extension method for setting the keytab; +in the init, we mostly just use raw krb5 methods so that we can report +the keytab contents, for -D+auth debugging. */ + +void +auth_heimdal_gssapi_init(auth_instance *ablock) +{ +krb5_context context; +krb5_keytab keytab; +krb5_kt_cursor cursor; +krb5_keytab_entry entry; +krb5_error_code krc; +char *principal, *enctype_s; +const char *k_keytab_typed_name = NULL; +auth_heimdal_gssapi_options_block *ob = + (auth_heimdal_gssapi_options_block *)(ablock->options_block); + +ablock->server = FALSE; +ablock->client = FALSE; + +if (!ob->server_service || !*ob->server_service) + { + HDEBUG(D_auth) debug_printf("heimdal: missing server_service\n"); + return; + } + +if ((krc = krb5_init_context(&context))) + { + int kerr = errno; + HDEBUG(D_auth) debug_printf("heimdal: failed to initialise krb5 context: %s\n", + strerror(kerr)); + return; + } + +if (ob->server_keytab) + { + k_keytab_typed_name = CCS string_sprintf("file:%s", expand_string(ob->server_keytab)); + HDEBUG(D_auth) debug_printf("heimdal: using keytab %s\n", k_keytab_typed_name); + if ((krc = krb5_kt_resolve(context, k_keytab_typed_name, &keytab))) + { + HDEBUG(D_auth) exim_heimdal_error_debug("krb5_kt_resolve", context, krc); + return; + } + } +else + { + HDEBUG(D_auth) debug_printf("heimdal: using system default keytab\n"); + if ((krc = krb5_kt_default(context, &keytab))) + { + HDEBUG(D_auth) exim_heimdal_error_debug("krb5_kt_default", context, krc); + return; + } + } + +HDEBUG(D_auth) + { + /* http://www.h5l.org/manual/HEAD/krb5/krb5_keytab_intro.html */ + if ((krc = krb5_kt_start_seq_get(context, keytab, &cursor))) + exim_heimdal_error_debug("krb5_kt_start_seq_get", context, krc); + else + { + while (!(krc = krb5_kt_next_entry(context, keytab, &entry, &cursor))) + { + principal = enctype_s = NULL; + krb5_unparse_name(context, entry.principal, &principal); + krb5_enctype_to_string(context, entry.keyblock.keytype, &enctype_s); + debug_printf("heimdal: keytab principal: %s vno=%d type=%s\n", + principal ? principal : "??", + entry.vno, + enctype_s ? enctype_s : "??"); + free(principal); + free(enctype_s); + krb5_kt_free_entry(context, &entry); + } + if ((krc = krb5_kt_end_seq_get(context, keytab, &cursor))) + exim_heimdal_error_debug("krb5_kt_end_seq_get", context, krc); + } + } + +if ((krc = krb5_kt_close(context, keytab))) + HDEBUG(D_auth) exim_heimdal_error_debug("krb5_kt_close", context, krc); + +krb5_free_context(context); + +ablock->server = TRUE; +} + + +static void +exim_heimdal_error_debug(const char *label, + krb5_context context, krb5_error_code err) +{ +const char *kerrsc; +kerrsc = krb5_get_error_message(context, err); +debug_printf("heimdal %s: %s\n", label, kerrsc ? kerrsc : "unknown error"); +krb5_free_error_message(context, kerrsc); +} + +/************************************************* +* Server entry point * +*************************************************/ + +/* For interface, see auths/README */ + +/* GSSAPI notes: +OM_uint32: portable type for unsigned int32 +gss_buffer_desc / *gss_buffer_t: hold/point-to size_t .length & void *value + -- all strings/etc passed in should go through one of these + -- when allocated by gssapi, release with gss_release_buffer() +*/ + +int +auth_heimdal_gssapi_server(auth_instance *ablock, uschar *initial_data) +{ +gss_name_t gclient = GSS_C_NO_NAME; +gss_name_t gserver = GSS_C_NO_NAME; +gss_cred_id_t gcred = GSS_C_NO_CREDENTIAL; +gss_ctx_id_t gcontext = GSS_C_NO_CONTEXT; +uschar *ex_server_str; +gss_buffer_desc gbufdesc = GSS_C_EMPTY_BUFFER; +gss_buffer_desc gbufdesc_in = GSS_C_EMPTY_BUFFER; +gss_buffer_desc gbufdesc_out = GSS_C_EMPTY_BUFFER; +gss_OID mech_type; +OM_uint32 maj_stat, min_stat; +int step, error_out; +uschar *tmp1, *tmp2, *from_client; +auth_heimdal_gssapi_options_block *ob = + (auth_heimdal_gssapi_options_block *)(ablock->options_block); +BOOL handled_empty_ir; +rmark store_reset_point; +uschar *keytab; +uschar sasl_config[4]; +uschar requested_qop; + +store_reset_point = store_mark(); + +HDEBUG(D_auth) + debug_printf("heimdal: initialising auth context for %s\n", ablock->name); + +/* Construct our gss_name_t gserver describing ourselves */ +tmp1 = expand_string(ob->server_service); +tmp2 = expand_string(ob->server_hostname); +ex_server_str = string_sprintf("%s@%s", tmp1, tmp2); +gbufdesc.value = (void *) ex_server_str; +gbufdesc.length = Ustrlen(ex_server_str); +maj_stat = gss_import_name(&min_stat, + &gbufdesc, GSS_C_NT_HOSTBASED_SERVICE, &gserver); +if (GSS_ERROR(maj_stat)) + return exim_gssapi_error_defer(store_reset_point, maj_stat, min_stat, + "gss_import_name(%s)", CS gbufdesc.value); + +/* Use a specific keytab, if specified */ +if (ob->server_keytab) + { + keytab = expand_string(ob->server_keytab); + maj_stat = gsskrb5_register_acceptor_identity(CCS keytab); + if (GSS_ERROR(maj_stat)) + return exim_gssapi_error_defer(store_reset_point, maj_stat, min_stat, + "registering keytab \"%s\"", keytab); + HDEBUG(D_auth) + debug_printf("heimdal: using keytab \"%s\"\n", keytab); + } + +/* Acquire our credentials */ +maj_stat = gss_acquire_cred(&min_stat, + gserver, /* desired name */ + 0, /* time */ + GSS_C_NULL_OID_SET, /* desired mechs */ + GSS_C_ACCEPT, /* cred usage */ + &gcred, /* handle */ + NULL /* actual mechs */, + NULL /* time rec */); +if (GSS_ERROR(maj_stat)) + return exim_gssapi_error_defer(store_reset_point, maj_stat, min_stat, + "gss_acquire_cred(%s)", ex_server_str); + +maj_stat = gss_release_name(&min_stat, &gserver); + +HDEBUG(D_auth) debug_printf("heimdal: have server credentials.\n"); + +/* Loop talking to client */ +step = 0; +from_client = initial_data; +handled_empty_ir = FALSE; +error_out = OK; + +/* buffer sizes: auth_get_data() uses big_buffer, which we grow per +GSSAPI RFC in _init, if needed, to meet the SHOULD size of 64KB. +(big_buffer starts life at the MUST size of 16KB). */ + +/* step values +0: getting initial data from client to feed into GSSAPI +1: iterating for as long as GSS_S_CONTINUE_NEEDED +2: GSS_S_COMPLETE, SASL wrapping for authz and qop to send to client +3: unpick final auth message from client +4: break/finish (non-step) +*/ +while (step < 4) + switch (step) + { + case 0: + if (!from_client || !*from_client) + { + if (handled_empty_ir) + { + HDEBUG(D_auth) debug_printf("gssapi: repeated empty input, grr.\n"); + error_out = BAD64; + goto ERROR_OUT; + } + + HDEBUG(D_auth) debug_printf("gssapi: missing initial response, nudging.\n"); + if ((error_out = auth_get_data(&from_client, US"", 0)) != OK) + goto ERROR_OUT; + handled_empty_ir = TRUE; + continue; + } + /* We should now have the opening data from the client, base64-encoded. */ + step += 1; + HDEBUG(D_auth) debug_printf("heimdal: have initial client data\n"); + break; + + case 1: + gbufdesc_in.length = b64decode(from_client, USS &gbufdesc_in.value); + if (gclient) + { + maj_stat = gss_release_name(&min_stat, &gclient); + gclient = GSS_C_NO_NAME; + } + maj_stat = gss_accept_sec_context(&min_stat, + &gcontext, /* context handle */ + gcred, /* acceptor cred handle */ + &gbufdesc_in, /* input from client */ + GSS_C_NO_CHANNEL_BINDINGS, /* XXX fixme: use the channel bindings from GnuTLS */ + &gclient, /* client identifier */ + &mech_type, /* mechanism in use */ + &gbufdesc_out, /* output to send to client */ + NULL, /* return flags */ + NULL, /* time rec */ + NULL /* delegated cred_handle */ + ); + if (GSS_ERROR(maj_stat)) + { + exim_gssapi_error_defer(NULL, maj_stat, min_stat, + "gss_accept_sec_context()"); + error_out = FAIL; + goto ERROR_OUT; + } + if (gbufdesc_out.length != 0) + { + error_out = auth_get_data(&from_client, + gbufdesc_out.value, gbufdesc_out.length); + if (error_out != OK) + goto ERROR_OUT; + + gss_release_buffer(&min_stat, &gbufdesc_out); + EmptyBuf(gbufdesc_out); + } + if (maj_stat == GSS_S_COMPLETE) + { + step += 1; + HDEBUG(D_auth) debug_printf("heimdal: GSS complete\n"); + } + else + HDEBUG(D_auth) debug_printf("heimdal: need more data\n"); + break; + + case 2: + memset(sasl_config, 0xFF, 4); + /* draft-ietf-sasl-gssapi-06.txt defines bitmasks for first octet + 0x01 No security layer + 0x02 Integrity protection + 0x04 Confidentiality protection + + The remaining three octets are the maximum buffer size for wrapped + content. */ + sasl_config[0] = 0x01; /* Exim does not wrap/unwrap SASL layers after auth */ + gbufdesc.value = (void *) sasl_config; + gbufdesc.length = 4; + maj_stat = gss_wrap(&min_stat, + gcontext, + 0, /* conf_req_flag: integrity only */ + GSS_C_QOP_DEFAULT, /* qop requested */ + &gbufdesc, /* message to protect */ + NULL, /* conf_state: no confidentiality applied */ + &gbufdesc_out /* output buffer */ + ); + if (GSS_ERROR(maj_stat)) + { + exim_gssapi_error_defer(NULL, maj_stat, min_stat, + "gss_wrap(SASL state after auth)"); + error_out = FAIL; + goto ERROR_OUT; + } + + HDEBUG(D_auth) debug_printf("heimdal SASL: requesting QOP with no security layers\n"); + + error_out = auth_get_data(&from_client, + gbufdesc_out.value, gbufdesc_out.length); + if (error_out != OK) + goto ERROR_OUT; + + gss_release_buffer(&min_stat, &gbufdesc_out); + EmptyBuf(gbufdesc_out); + step += 1; + break; + + case 3: + gbufdesc_in.length = b64decode(from_client, USS &gbufdesc_in.value); + maj_stat = gss_unwrap(&min_stat, + gcontext, + &gbufdesc_in, /* data from client */ + &gbufdesc_out, /* results */ + NULL, /* conf state */ + NULL /* qop state */ + ); + if (GSS_ERROR(maj_stat)) + { + exim_gssapi_error_defer(NULL, maj_stat, min_stat, + "gss_unwrap(final SASL message from client)"); + error_out = FAIL; + goto ERROR_OUT; + } + if (gbufdesc_out.length < 4) + { + HDEBUG(D_auth) + debug_printf("gssapi: final message too short; " + "need flags, buf sizes and optional authzid\n"); + error_out = FAIL; + goto ERROR_OUT; + } + + requested_qop = (CS gbufdesc_out.value)[0]; + if (!(requested_qop & 0x01)) + { + HDEBUG(D_auth) + debug_printf("gssapi: client requested security layers (%x)\n", + (unsigned int) requested_qop); + error_out = FAIL; + goto ERROR_OUT; + } + + for (int i = 0; i < AUTH_VARS; i++) auth_vars[i] = NULL; + expand_nmax = 0; + + /* Identifiers: + The SASL provided identifier is an unverified authzid. + GSSAPI provides us with a verified identifier, but it might be empty + for some clients. + */ + + /* $auth2 is authzid requested at SASL layer */ + if (gbufdesc_out.length > 4) + { + expand_nlength[2] = gbufdesc_out.length - 4; + auth_vars[1] = expand_nstring[2] = + string_copyn((US gbufdesc_out.value) + 4, expand_nlength[2]); + expand_nmax = 2; + } + + gss_release_buffer(&min_stat, &gbufdesc_out); + EmptyBuf(gbufdesc_out); + + /* $auth1 is GSSAPI display name */ + maj_stat = gss_display_name(&min_stat, + gclient, &gbufdesc_out, &mech_type); + if (GSS_ERROR(maj_stat)) + { + auth_vars[1] = expand_nstring[2] = NULL; + expand_nmax = 0; + exim_gssapi_error_defer(NULL, maj_stat, min_stat, + "gss_display_name(client identifier)"); + error_out = FAIL; + goto ERROR_OUT; + } + + expand_nlength[1] = gbufdesc_out.length; + auth_vars[0] = expand_nstring[1] = + string_copyn(gbufdesc_out.value, gbufdesc_out.length); + + if (expand_nmax == 0) /* should be: authzid was empty */ + { + expand_nmax = 2; + expand_nlength[2] = expand_nlength[1]; + auth_vars[1] = expand_nstring[2] = string_copyn(expand_nstring[1], expand_nlength[1]); + HDEBUG(D_auth) + debug_printf("heimdal SASL: empty authzid, set to dup of GSSAPI display name\n"); + } + + HDEBUG(D_auth) + debug_printf("heimdal SASL: happy with client request\n" + " auth1 (verified GSSAPI display-name): \"%s\"\n" + " auth2 (unverified SASL requested authzid): \"%s\"\n", + auth_vars[0], auth_vars[1]); + + step += 1; + break; + + } /* switch */ + /* while step */ + + +ERROR_OUT: +maj_stat = gss_release_cred(&min_stat, &gcred); +if (gclient) + { + gss_release_name(&min_stat, &gclient); + gclient = GSS_C_NO_NAME; + } +if (gbufdesc_out.length) + { + gss_release_buffer(&min_stat, &gbufdesc_out); + EmptyBuf(gbufdesc_out); + } +if (gcontext != GSS_C_NO_CONTEXT) + gss_delete_sec_context(&min_stat, &gcontext, GSS_C_NO_BUFFER); + +store_reset(store_reset_point); + +if (error_out != OK) + return error_out; + +/* Auth succeeded, check server_condition */ +return auth_check_serv_cond(ablock); +} + + +static int +exim_gssapi_error_defer(rmark store_reset_point, + OM_uint32 major, OM_uint32 minor, + const char *format, ...) +{ +va_list ap; +OM_uint32 maj_stat, min_stat; +OM_uint32 msgcontext = 0; +gss_buffer_desc status_string; +gstring * g; + +HDEBUG(D_auth) + { + va_start(ap, format); + g = string_vformat(NULL, SVFMT_EXTEND|SVFMT_REBUFFER, format, ap); + va_end(ap); + } + +auth_defer_msg = NULL; + +do { + maj_stat = gss_display_status(&min_stat, + major, GSS_C_GSS_CODE, GSS_C_NO_OID, &msgcontext, &status_string); + + if (!auth_defer_msg) + auth_defer_msg = string_copy(US status_string.value); + + HDEBUG(D_auth) debug_printf("heimdal %s: %.*s\n", + string_from_gstring(g), (int)status_string.length, + CS status_string.value); + gss_release_buffer(&min_stat, &status_string); + + } while (msgcontext != 0); + +if (store_reset_point) + store_reset(store_reset_point); +return DEFER; +} + + +/************************************************* +* Client entry point * +*************************************************/ + +/* For interface, see auths/README */ + +int +auth_heimdal_gssapi_client( + auth_instance *ablock, /* authenticator block */ + void * sx, /* connection */ + int timeout, /* command timeout */ + uschar *buffer, /* buffer for reading response */ + int buffsize) /* size of buffer */ +{ +HDEBUG(D_auth) + debug_printf("Client side NOT IMPLEMENTED: you should not see this!\n"); +/* NOT IMPLEMENTED */ +return FAIL; +} + +/************************************************* +* Diagnostic API * +*************************************************/ + +gstring * +auth_heimdal_gssapi_version_report(gstring * g) +{ +/* No build-time constants available unless we link against libraries at +build-time and export the result as a string into a header ourselves. */ + +return string_fmt_append(g, "Library version: Heimdal: Runtime: %s\n" + " Build Info: %s\n", + heimdal_version, heimdal_long_version)); +} + +#endif /*!MACRO_PREDEF*/ +#endif /* AUTH_HEIMDAL_GSSAPI */ + +/* End of heimdal_gssapi.c */ diff --git a/src/auths/heimdal_gssapi.h b/src/auths/heimdal_gssapi.h new file mode 100644 index 0000000..49775af --- /dev/null +++ b/src/auths/heimdal_gssapi.h @@ -0,0 +1,39 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2022 */ +/* Copyright (c) University of Cambridge 1995 - 2012 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Copyright (c) Twitter Inc 2012 + Author: Phil Pennock */ +/* Copyright (c) Phil Pennock 2012 */ + +/* Interface to Heimdal library for GSSAPI authentication. */ + +/* Authenticator-specific options. */ + +typedef struct { + uschar *server_hostname; + uschar *server_keytab; + uschar *server_service; +} auth_heimdal_gssapi_options_block; + +/* Data for reading the authenticator-specific options. */ + +extern optionlist auth_heimdal_gssapi_options[]; +extern int auth_heimdal_gssapi_options_count; + +/* Defaults for the authenticator-specific options. */ + +extern auth_heimdal_gssapi_options_block auth_heimdal_gssapi_option_defaults; + +/* The entry points for the mechanism */ + +extern void auth_heimdal_gssapi_init(auth_instance *); +extern int auth_heimdal_gssapi_server(auth_instance *, uschar *); +extern int auth_heimdal_gssapi_client(auth_instance *, void *, int, uschar *, int); +extern void auth_heimdal_gssapi_version_report(BOOL); + +/* End of heimdal_gssapi.h */ diff --git a/src/auths/plaintext.c b/src/auths/plaintext.c new file mode 100644 index 0000000..58d1783 --- /dev/null +++ b/src/auths/plaintext.c @@ -0,0 +1,179 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* Copyright (c) The Exim Maintainers 2020 - 2021 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" +#include "plaintext.h" + + +/* Options specific to the plaintext authentication mechanism. */ + +optionlist auth_plaintext_options[] = { + { "client_ignore_invalid_base64", opt_bool, + OPT_OFF(auth_plaintext_options_block, client_ignore_invalid_base64) }, + { "client_send", opt_stringptr, + OPT_OFF(auth_plaintext_options_block, client_send) }, + { "server_prompts", opt_stringptr, + OPT_OFF(auth_plaintext_options_block, server_prompts) } +}; + +/* Size of the options list. An extern variable has to be used so that its +address can appear in the tables drtables.c. */ + +int auth_plaintext_options_count = + sizeof(auth_plaintext_options)/sizeof(optionlist); + +/* Default private options block for the plaintext authentication method. */ + +auth_plaintext_options_block auth_plaintext_option_defaults = { + NULL, /* server_prompts */ + NULL, /* client_send */ + FALSE /* client_ignore_invalid_base64 */ +}; + + +#ifdef MACRO_PREDEF + +/* Dummy values */ +void auth_plaintext_init(auth_instance *ablock) {} +int auth_plaintext_server(auth_instance *ablock, uschar *data) {return 0;} +int auth_plaintext_client(auth_instance *ablock, void * sx, int timeout, + uschar *buffer, int buffsize) {return 0;} + +#else /*!MACRO_PREDEF*/ + + + +/************************************************* +* Initialization entry point * +*************************************************/ + +/* Called for each instance, after its options have been read, to +enable consistency checks to be done, or anything else that needs +to be set up. */ + +void +auth_plaintext_init(auth_instance *ablock) +{ +auth_plaintext_options_block *ob = + (auth_plaintext_options_block *)(ablock->options_block); +if (ablock->public_name == NULL) ablock->public_name = ablock->name; +if (ablock->server_condition != NULL) ablock->server = TRUE; +if (ob->client_send != NULL) ablock->client = TRUE; +} + + + +/************************************************* +* Server entry point * +*************************************************/ + +/* For interface, see auths/README */ + +int +auth_plaintext_server(auth_instance * ablock, uschar * data) +{ +auth_plaintext_options_block * ob = + (auth_plaintext_options_block *)(ablock->options_block); +const uschar * prompts = ob->server_prompts; +uschar * s; +int number = 1; +int rc; +int sep = 0; + +/* Expand a non-empty list of prompt strings */ + +if (prompts) + if (!(prompts = expand_cstring(prompts))) + { + auth_defer_msg = expand_string_message; + return DEFER; + } + +/* If data was supplied on the AUTH command, decode it, and split it up into +multiple items at binary zeros. The strings are put into $auth1, $auth2, etc, +up to a maximum. To retain backwards compatibility, they are also put int $1, +$2, etc. If the data consists of the string "=" it indicates a single, empty +string. */ + +if (*data) + if ((rc = auth_read_input(data)) != OK) + return rc; + +/* Now go through the list of prompt strings. Skip over any whose data has +already been provided as part of the AUTH command. For the rest, send them +out as prompts, and get a data item back. If the data item is "*", abandon the +authentication attempt. Otherwise, split it into items as above. */ + +while ( (s = string_nextinlist(&prompts, &sep, NULL, 0)) + && expand_nmax < EXPAND_MAXN) + if (number++ > expand_nmax) + if ((rc = auth_prompt(CUS s)) != OK) + return rc; + +/* We now have a number of items of data in $auth1, $auth2, etc (and also, for +compatibility, in $1, $2, etc). Authentication and authorization are handled +together for this authenticator by expanding the server_condition option. Note +that ablock->server_condition is always non-NULL because that's what configures +this authenticator as a server. */ + +return auth_check_serv_cond(ablock); +} + + + +/************************************************* +* Client entry point * +*************************************************/ + +/* For interface, see auths/README */ + +int +auth_plaintext_client( + auth_instance *ablock, /* authenticator block */ + void * sx, /* smtp connextion */ + int timeout, /* command timeout */ + uschar *buffer, /* buffer for reading response */ + int buffsize) /* size of buffer */ +{ +auth_plaintext_options_block *ob = + (auth_plaintext_options_block *)(ablock->options_block); +const uschar * text = ob->client_send; +const uschar * s; +int sep = 0; +int auth_var_idx = 0, rc; +int flags = AUTH_ITEM_FIRST; + +if (ob->client_ignore_invalid_base64) + flags |= AUTH_ITEM_IGN64; + +/* The text is broken up into a number of different data items, which are +sent one by one. The first one is sent with the AUTH command; the remainder are +sent in response to subsequent prompts. Each is expanded before being sent. */ + +while ((s = string_nextinlist(&text, &sep, NULL, 0))) + { + if (!text) + flags |= AUTH_ITEM_LAST; + + if ((rc = auth_client_item(sx, ablock, &s, flags, timeout, buffer, buffsize)) + != DEFER) + return rc; + + flags &= ~AUTH_ITEM_FIRST; + + if (auth_var_idx < AUTH_VARS) + auth_vars[auth_var_idx++] = string_copy(s); + } + +/* Control should never actually get here. */ + +return FAIL; +} + +#endif /*!MACRO_PREDEF*/ +/* End of plaintext.c */ diff --git a/src/auths/plaintext.h b/src/auths/plaintext.h new file mode 100644 index 0000000..4c6d011 --- /dev/null +++ b/src/auths/plaintext.h @@ -0,0 +1,31 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2009 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Private structure for the private options. */ + +typedef struct { + uschar *server_prompts; + uschar *client_send; + BOOL client_ignore_invalid_base64; +} auth_plaintext_options_block; + +/* Data for reading the private options. */ + +extern optionlist auth_plaintext_options[]; +extern int auth_plaintext_options_count; + +/* Block containing default values. */ + +extern auth_plaintext_options_block auth_plaintext_option_defaults; + +/* The entry points for the mechanism */ + +extern void auth_plaintext_init(auth_instance *); +extern int auth_plaintext_server(auth_instance *, uschar *); +extern int auth_plaintext_client(auth_instance *, void *, int, uschar *, int); + +/* End of plaintext.h */ diff --git a/src/auths/pwcheck.c b/src/auths/pwcheck.c new file mode 100644 index 0000000..7dd529f --- /dev/null +++ b/src/auths/pwcheck.c @@ -0,0 +1,449 @@ +/* SASL server API implementation + * Rob Siemborski + * Tim Martin + * $Id: checkpw.c,v 1.49 2002/03/07 19:14:04 ken3 Exp $ + */ +/* Copyright (c) The Exim Maintainers 2021 - 2022 */ +/* + * Copyright (c) 2001 Carnegie Mellon University. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The name "Carnegie Mellon University" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For permission or any other legal + * details, please contact + * Office of Technology Transfer + * Carnegie Mellon University + * 5000 Forbes Avenue + * Pittsburgh, PA 15213-3890 + * (412) 268-4387, fax: (412) 268-7395 + * tech-transfer@andrew.cmu.edu + * + * 4. Redistributions of any form whatsoever must retain the following + * acknowledgment: + * "This product includes software developed by Computing Services + * at Carnegie Mellon University (http://www.cmu.edu/computing/)." + * + * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO + * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE + * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * Taken from Cyrus-SASL library and adapted by Alexander S. Sabourenkov + * Oct 2001 - Apr 2002: Slightly modified by Philip Hazel. + * Aug 2003: new code for saslauthd from Alexander S. Sabourenkov incorporated + * by Philip Hazel (minor mods to avoid compiler warnings) + * Oct 2006: (PH) removed redundant tests on "reply" being NULL - some were + * missing, and confused someone who was using this code for some + * other purpose. Here in Exim, "reply" is never NULL. + * + * screwdriver@lxnt.info + * + */ + +/* Originally this module supported only the pwcheck daemon, which is where its +name comes from. Nowadays it supports saslauthd as well; pwcheck is in fact +deprecated. The definitions of CYRUS_PWCHECK_SOCKET and CYRUS_SASLAUTHD_SOCKET +determine whether the facilities are actually supported or not. */ + + +#include "../exim.h" +#include "pwcheck.h" + + +#if defined(CYRUS_PWCHECK_SOCKET) || defined(CYRUS_SASLAUTHD_SOCKET) + +#include + +static int retry_read(int, void *, unsigned ); +static int retry_writev(int, struct iovec *, int ); +static int read_string(int, uschar **); +static int write_string(int, const uschar *, int); + +#endif + + +/* A dummy function that always fails if pwcheck support is not +wanted. */ + +#ifndef CYRUS_PWCHECK_SOCKET +int pwcheck_verify_password(const char *userid, + const char *passwd, + const char **reply) +{ +*reply = "pwcheck support is not included in this Exim binary"; +return PWCHECK_FAIL; +} + + +/* This is the real function */ + +#else + + /* taken from cyrus-sasl file checkpw.c */ + /* pwcheck daemon-authenticated login */ + int pwcheck_verify_password(const char *userid, + const char *passwd, + const char **reply) + { + int s, start, r, n; + struct sockaddr_un srvaddr; + struct iovec iov[2]; + static char response[1024]; + + *reply = NULL; + + s = socket(AF_UNIX, SOCK_STREAM, 0); + if (s == -1) { return PWCHECK_FAIL; } + + memset(CS &srvaddr, 0, sizeof(srvaddr)); + srvaddr.sun_family = AF_UNIX; + strncpy(srvaddr.sun_path, CYRUS_PWCHECK_SOCKET, sizeof(srvaddr.sun_path)); + r = connect(s, (struct sockaddr *)&srvaddr, sizeof(srvaddr)); + if (r == -1) { + DEBUG(D_auth) + debug_printf("Cannot connect to pwcheck daemon (at '%s')\n",CYRUS_PWCHECK_SOCKET); + *reply = "cannot connect to pwcheck daemon"; + return PWCHECK_FAIL; + } + + iov[0].iov_base = CS userid; + iov[0].iov_len = strlen(userid)+1; + iov[1].iov_base = CS passwd; + iov[1].iov_len = strlen(passwd)+1; + + retry_writev(s, iov, 2); + + start = 0; + while (start < sizeof(response) - 1) { + n = read(s, response+start, sizeof(response) - 1 - start); + if (n < 1) break; + start += n; + } + + (void)close(s); + + if (start > 1 && !strncmp(response, "OK", 2)) { + return PWCHECK_OK; + } + + response[start] = '\0'; + *reply = response; + return PWCHECK_NO; + } + +#endif + + + + /* A dummy function that always fails if saslauthd support is not +wanted. */ + +#ifndef CYRUS_SASLAUTHD_SOCKET +int saslauthd_verify_password(const uschar *userid, + const uschar *passwd, + const uschar *service, + const uschar *realm, + const uschar **reply) +{ +*reply = US"saslauthd support is not included in this Exim binary"; +return PWCHECK_FAIL; +} + + +/* This is the real function */ + +#else + /* written from scratch */ + /* saslauthd daemon-authenticated login */ + +int saslauthd_verify_password(const uschar *userid, + const uschar *password, + const uschar *service, + const uschar *realm, + const uschar **reply) +{ + uschar *daemon_reply = NULL; + int s, r; + struct sockaddr_un srvaddr; + + DEBUG(D_auth) + debug_printf("saslauthd userid='%s' servicename='%s'" + " realm='%s'\n", userid, service, realm ); + + *reply = NULL; + + s = socket(AF_UNIX, SOCK_STREAM, 0); + if (s == -1) { + *reply = CUstrerror(errno); + return PWCHECK_FAIL; + } + + memset(CS &srvaddr, 0, sizeof(srvaddr)); + srvaddr.sun_family = AF_UNIX; + strncpy(srvaddr.sun_path, CYRUS_SASLAUTHD_SOCKET, + sizeof(srvaddr.sun_path)); + r = connect(s, (struct sockaddr *)&srvaddr, sizeof(srvaddr)); + if (r == -1) { + DEBUG(D_auth) + debug_printf("Cannot connect to saslauthd daemon (at '%s'): %s\n", + CYRUS_SASLAUTHD_SOCKET, strerror(errno)); + *reply = string_sprintf("cannot connect to saslauthd daemon at " + "%s: %s", CYRUS_SASLAUTHD_SOCKET, + strerror(errno)); + return PWCHECK_FAIL; + } + + if ( write_string(s, userid, Ustrlen(userid)) < 0) { + DEBUG(D_auth) + debug_printf("Failed to send userid to saslauthd daemon \n"); + (void)close(s); + return PWCHECK_FAIL; + } + + if ( write_string(s, password, Ustrlen(password)) < 0) { + DEBUG(D_auth) + debug_printf("Failed to send password to saslauthd daemon \n"); + (void)close(s); + return PWCHECK_FAIL; + } + + memset((void *)password, 0, Ustrlen(password)); + + if ( write_string(s, service, Ustrlen(service)) < 0) { + DEBUG(D_auth) + debug_printf("Failed to send service name to saslauthd daemon \n"); + (void)close(s); + return PWCHECK_FAIL; + } + + if ( write_string(s, realm, Ustrlen(realm)) < 0) { + DEBUG(D_auth) + debug_printf("Failed to send realm to saslauthd daemon \n"); + (void)close(s); + return PWCHECK_FAIL; + } + + if ( read_string(s, &daemon_reply ) < 2) { + DEBUG(D_auth) + debug_printf("Corrupted answer '%s' received. \n", daemon_reply); + (void)close(s); + return PWCHECK_FAIL; + } + + (void)close(s); + + DEBUG(D_auth) + debug_printf("Answer '%s' received. \n", daemon_reply); + + *reply = daemon_reply; + + if ( (daemon_reply[0] == 'O') && (daemon_reply[1] == 'K') ) + return PWCHECK_OK; + + if ( (daemon_reply[0] == 'N') && (daemon_reply[1] == 'O') ) + return PWCHECK_NO; + + return PWCHECK_FAIL; +} + +#endif + + +/* helper functions */ +#if defined(CYRUS_PWCHECK_SOCKET) || defined(CYRUS_SASLAUTHD_SOCKET) + +#define MAX_REQ_LEN 1024 + +/* written from scratch */ + +/* FUNCTION: read_string */ + +/* SYNOPSIS + * read a sasld-style counted string into + * store-allocated buffer, set pointer to the buffer, + * return number of bytes read or -1 on failure. + * END SYNOPSIS */ + +static int read_string(int fd, uschar **retval) { + unsigned short count; + int rc; + + rc = (retry_read(fd, &count, sizeof(count)) < (int) sizeof(count)); + if (!rc) { + count = ntohs(count); + if (count > MAX_REQ_LEN) { + return -1; + } else { + /* Assume the file is trusted, so no tainting */ + *retval = store_get(count + 1, GET_UNTAINTED); + rc = (retry_read(fd, *retval, count) < (int) count); + (*retval)[count] = '\0'; + return count; + } + } + return -1; +} + + +/* FUNCTION: write_string */ + +/* SYNOPSIS + * write a sasld-style counted string into given fd + * written bytes on success, -1 on failure. + * END SYNOPSIS */ + +static int write_string(int fd, const uschar *string, int len) { + unsigned short count; + int rc; + struct iovec iov[2]; + + count = htons(len); + + iov[0].iov_base = (void *) &count; + iov[0].iov_len = sizeof(count); + iov[1].iov_base = (void *) string; + iov[1].iov_len = len; + + rc = retry_writev(fd, iov, 2); + + return rc; +} + + +/* taken from cyrus-sasl file saslauthd/saslauthd-unix.c */ + +/* FUNCTION: retry_read */ + +/* SYNOPSIS + * Keep calling the read() system call with 'fd', 'buf', and 'nbyte' + * until all the data is read in or an error occurs. + * END SYNOPSIS */ +static int retry_read(int fd, void *inbuf, unsigned nbyte) +{ + int n; + int nread = 0; + char *buf = CS inbuf; + + if (nbyte == 0) return 0; + + for (;;) { + n = read(fd, buf, nbyte); + if (n == 0) { + /* end of file */ + return -1; + } + if (n == -1) { + if (errno == EINTR) continue; + return -1; + } + + nread += n; + + if (n >= (int) nbyte) return nread; + + buf += n; + nbyte -= n; + } +} + +/* END FUNCTION: retry_read */ + +/* FUNCTION: retry_writev */ + +/* SYNOPSIS + * Keep calling the writev() system call with 'fd', 'iov', and 'iovcnt' + * until all the data is written out or an error occurs. + * END SYNOPSIS */ + +static int /* R: bytes written, or -1 on error */ +retry_writev ( + /* PARAMETERS */ + int fd, /* I: fd to write on */ + struct iovec *iov, /* U: iovec array base + * modified as data written */ + int iovcnt /* I: number of iovec entries */ + /* END PARAMETERS */ + ) +{ + /* VARIABLES */ + int n; /* return value from writev() */ + int i; /* loop counter */ + int written; /* bytes written so far */ + static int iov_max; /* max number of iovec entries */ + /* END VARIABLES */ + + /* initialization */ +#ifdef MAXIOV + iov_max = MAXIOV; +#else /* ! MAXIOV */ +# ifdef IOV_MAX + iov_max = IOV_MAX; +# else /* ! IOV_MAX */ + iov_max = 8192; +# endif /* ! IOV_MAX */ +#endif /* ! MAXIOV */ + written = 0; + + for (;;) { + + while (iovcnt && iov[0].iov_len == 0) { + iov++; + iovcnt--; + } + + if (!iovcnt) { + return written; + } + + n = writev(fd, iov, iovcnt > iov_max ? iov_max : iovcnt); + if (n == -1) { + if (errno == EINVAL && iov_max > 10) { + iov_max /= 2; + continue; + } + if (errno == EINTR) { + continue; + } + return -1; + } else { + written += n; + } + + for (i = 0; i < iovcnt; i++) { + if (iov[i].iov_len > (unsigned) n) { + iov[i].iov_base = CS iov[i].iov_base + n; + iov[i].iov_len -= n; + break; + } + n -= iov[i].iov_len; + iov[i].iov_len = 0; + } + + if (i == iovcnt) { + return written; + } + } + /* NOTREACHED */ +} + +/* END FUNCTION: retry_writev */ +#endif + +/* End of auths/pwcheck.c */ diff --git a/src/auths/pwcheck.h b/src/auths/pwcheck.h new file mode 100644 index 0000000..1287ea2 --- /dev/null +++ b/src/auths/pwcheck.h @@ -0,0 +1,27 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2009 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* This file provides support for authentication via the Cyrus SASL pwcheck +daemon (whence its name) and the newer saslauthd daemon. */ + +/* Error codes used internally within the authentication functions */ + +/* PWCHECK_OK - auth successful + PWCHECK_NO - access denied + PWCHECK_FAIL - [temporary] failure */ + +#define PWCHECK_OK 0 +#define PWCHECK_NO 1 +#define PWCHECK_FAIL 2 + +/* Cyrus functions for doing the business. */ + +extern int pwcheck_verify_password(const char *, const char *, const char **); +extern int saslauthd_verify_password(const uschar *, const uschar *, + const uschar *, const uschar *, const uschar **); + +/* End of pwcheck.h */ diff --git a/src/auths/spa.c b/src/auths/spa.c new file mode 100644 index 0000000..ff90d33 --- /dev/null +++ b/src/auths/spa.c @@ -0,0 +1,376 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* Copyright (c) The Exim Maintainers 2020 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* This file, which provides support for Microsoft's Secure Password +Authentication, was contributed by Marc Prud'hommeaux. Tom Kistner added SPA +server support. I (PH) have only modified it in very trivial ways. + +References: + http://www.innovation.ch/java/ntlm.html + http://www.kuro5hin.org/story/2002/4/28/1436/66154 + http://download.microsoft.com/download/9/5/e/95ef66af-9026-4bb0-a41d-a4f81802d92c/%5bMS-SMTP%5d.pdf + + * It seems that some systems have existing but different definitions of some + * of the following types. I received a complaint about "int16" causing + * compilation problems. So I (PH) have renamed them all, to be on the safe + * side, by adding 'x' on the end. See auths/auth-spa.h. + + * typedef signed short int16; + * typedef unsigned short uint16; + * typedef unsigned uint32; + * typedef unsigned char uint8; + +07-August-2003: PH: Patched up the code to avoid assert bombouts for stupid + input data. Find appropriate comment by grepping for "PH". +16-October-2006: PH: Added a call to auth_check_serv_cond() at the end +05-June-2010: PP: handle SASL initial response +*/ + + +#include "../exim.h" +#include "spa.h" + +/* #define DEBUG_SPA */ + +#ifdef DEBUG_SPA +#define DSPA(x,y,z) debug_printf(x,y,z) +#else +#define DSPA(x,y,z) +#endif + +/* Options specific to the spa authentication mechanism. */ + +optionlist auth_spa_options[] = { + { "client_domain", opt_stringptr, + OPT_OFF(auth_spa_options_block, spa_domain) }, + { "client_password", opt_stringptr, + OPT_OFF(auth_spa_options_block, spa_password) }, + { "client_username", opt_stringptr, + OPT_OFF(auth_spa_options_block, spa_username) }, + { "server_password", opt_stringptr, + OPT_OFF(auth_spa_options_block, spa_serverpassword) } +}; + +/* Size of the options list. An extern variable has to be used so that its +address can appear in the tables drtables.c. */ + +int auth_spa_options_count = + sizeof(auth_spa_options)/sizeof(optionlist); + +/* Default private options block for the condition authentication method. */ + +auth_spa_options_block auth_spa_option_defaults = { + NULL, /* spa_password */ + NULL, /* spa_username */ + NULL, /* spa_domain */ + NULL /* spa_serverpassword (for server side use) */ +}; + + +#ifdef MACRO_PREDEF + +/* Dummy values */ +void auth_spa_init(auth_instance *ablock) {} +int auth_spa_server(auth_instance *ablock, uschar *data) {return 0;} +int auth_spa_client(auth_instance *ablock, void * sx, int timeout, + uschar *buffer, int buffsize) {return 0;} + +#else /*!MACRO_PREDEF*/ + + + + +/************************************************* +* Initialization entry point * +*************************************************/ + +/* Called for each instance, after its options have been read, to +enable consistency checks to be done, or anything else that needs +to be set up. */ + +void +auth_spa_init(auth_instance *ablock) +{ +auth_spa_options_block *ob = + (auth_spa_options_block *)(ablock->options_block); + +/* The public name defaults to the authenticator name */ + +if (ablock->public_name == NULL) ablock->public_name = ablock->name; + +/* Both username and password must be set for a client */ + +if ((ob->spa_username == NULL) != (ob->spa_password == NULL)) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s authenticator:\n " + "one of client_username and client_password cannot be set without " + "the other", ablock->name); +ablock->client = ob->spa_username != NULL; + +/* For a server we have just one option */ + +ablock->server = ob->spa_serverpassword != NULL; +} + + + +/************************************************* +* Server entry point * +*************************************************/ + +/* For interface, see auths/README */ + +#define CVAL(buf,pos) ((US (buf))[pos]) +#define PVAL(buf,pos) ((unsigned)CVAL(buf,pos)) +#define SVAL(buf,pos) (PVAL(buf,pos)|PVAL(buf,(pos)+1)<<8) +#define IVAL(buf,pos) (SVAL(buf,pos)|SVAL(buf,(pos)+2)<<16) + +int +auth_spa_server(auth_instance *ablock, uschar *data) +{ +auth_spa_options_block *ob = (auth_spa_options_block *)(ablock->options_block); +uint8x lmRespData[24]; +uint8x ntRespData[24]; +SPAAuthRequest request; +SPAAuthChallenge challenge; +SPAAuthResponse response; +SPAAuthResponse *responseptr = &response; +uschar msgbuf[2048]; +uschar *clearpass, *s; +unsigned off; + +/* send a 334, MS Exchange style, and grab the client's request, +unless we already have it via an initial response. */ + +if (!*data && auth_get_no64_data(&data, US"NTLM supported") != OK) + return FAIL; + +if (spa_base64_to_bits(CS &request, sizeof(request), CCS data) < 0) + { + DEBUG(D_auth) debug_printf("auth_spa_server(): bad base64 data in " + "request: %s\n", data); + return FAIL; + } + +/* create a challenge and send it back */ + +spa_build_auth_challenge(&request, &challenge); +spa_bits_to_base64(msgbuf, US &challenge, spa_request_length(&challenge)); + +if (auth_get_no64_data(&data, msgbuf) != OK) + return FAIL; + +/* dump client response */ +if (spa_base64_to_bits(CS &response, sizeof(response), CCS data) < 0) + { + DEBUG(D_auth) debug_printf("auth_spa_server(): bad base64 data in " + "response: %s\n", data); + return FAIL; + } + +/*************************************************************** +PH 07-Aug-2003: The original code here was this: + +Ustrcpy(msgbuf, unicodeToString(((char*)responseptr) + + IVAL(&responseptr->uUser.offset,0), + SVAL(&responseptr->uUser.len,0)/2) ); + +However, if the response data is too long, unicodeToString bombs out on +an assertion failure. It uses a 1024 fixed buffer. Bombing out is not a good +idea. It's too messy to try to rework that function to return an error because +it is called from a number of other places in the auth-spa.c module. Instead, +since it is a very small function, I reproduce its code here, with a size check +that causes failure if the size of msgbuf is exceeded. ****/ + + { + int i; + char * p; + int len = SVAL(&responseptr->uUser.len,0)/2; + + if ( (off = IVAL(&responseptr->uUser.offset,0)) >= sizeof(SPAAuthResponse) + || len >= sizeof(responseptr->buffer)/2 + || (p = (CS responseptr) + off) + len*2 >= CS (responseptr+1) + ) + { + DEBUG(D_auth) + debug_printf("auth_spa_server(): bad uUser spec in response\n"); + return FAIL; + } + + if (len + 1 >= sizeof(msgbuf)) return FAIL; + for (i = 0; i < len; ++i) + { + msgbuf[i] = *p & 0x7f; + p += 2; + } + msgbuf[i] = 0; + } + +/***************************************************************/ + +/* Put the username in $auth1 and $1. The former is now the preferred variable; +the latter is the original variable. These have to be out of stack memory, and +need to be available once known even if not authenticated, for error messages +(server_set_id, which only makes it to authenticated_id if we return OK) */ + +auth_vars[0] = expand_nstring[1] = string_copy(msgbuf); +expand_nlength[1] = Ustrlen(msgbuf); +expand_nmax = 1; + +debug_print_string(ablock->server_debug_string); /* customized debug */ + +/* look up password */ + +if (!(clearpass = expand_string(ob->spa_serverpassword))) + if (f.expand_string_forcedfail) + { + DEBUG(D_auth) debug_printf("auth_spa_server(): forced failure while " + "expanding spa_serverpassword\n"); + return FAIL; + } + else + { + DEBUG(D_auth) debug_printf("auth_spa_server(): error while expanding " + "spa_serverpassword: %s\n", expand_string_message); + return DEFER; + } + +/* create local hash copy */ + +spa_smb_encrypt(clearpass, challenge.challengeData, lmRespData); +spa_smb_nt_encrypt(clearpass, challenge.challengeData, ntRespData); + +/* compare NT hash (LM may not be available) */ + +off = IVAL(&responseptr->ntResponse.offset,0); +if (off >= sizeof(SPAAuthResponse) - 24) + { + DEBUG(D_auth) + debug_printf("auth_spa_server(): bad ntRespData spec in response\n"); + return FAIL; + } +s = (US responseptr) + off; + +if (memcmp(ntRespData, s, 24) == 0) + return auth_check_serv_cond(ablock); /* success. we have a winner. */ + + /* Expand server_condition as an authorization check (PH) */ + +return FAIL; +} + + +/************************************************* +* Client entry point * +*************************************************/ + +/* For interface, see auths/README */ + +int +auth_spa_client( + auth_instance *ablock, /* authenticator block */ + void * sx, /* connection */ + int timeout, /* command timeout */ + uschar *buffer, /* buffer for reading response */ + int buffsize) /* size of buffer */ +{ +auth_spa_options_block *ob = + (auth_spa_options_block *)(ablock->options_block); +SPAAuthRequest request; +SPAAuthChallenge challenge; +SPAAuthResponse response; +char msgbuf[2048]; +char *domain = NULL; +char *username, *password; + +/* Code added by PH to expand the options */ + +*buffer = 0; /* Default no message when cancelled */ + +if (!(username = CS expand_string(ob->spa_username))) + { + if (f.expand_string_forcedfail) return CANCELLED; + string_format(buffer, buffsize, "expansion of \"%s\" failed in %s " + "authenticator: %s", ob->spa_username, ablock->name, + expand_string_message); + return ERROR; + } + +if (!(password = CS expand_string(ob->spa_password))) + { + if (f.expand_string_forcedfail) return CANCELLED; + string_format(buffer, buffsize, "expansion of \"%s\" failed in %s " + "authenticator: %s", ob->spa_password, ablock->name, + expand_string_message); + return ERROR; + } + +if (ob->spa_domain) + if (!(domain = CS expand_string(ob->spa_domain))) + { + if (f.expand_string_forcedfail) return CANCELLED; + string_format(buffer, buffsize, "expansion of \"%s\" failed in %s " + "authenticator: %s", ob->spa_domain, ablock->name, + expand_string_message); + return ERROR; + } + +/* Original code */ + +if (smtp_write_command(sx, SCMD_FLUSH, "AUTH %s\r\n", ablock->public_name) < 0) + return FAIL_SEND; + +/* wait for the 3XX OK message */ +if (!smtp_read_response(sx, US buffer, buffsize, '3', timeout)) + return FAIL; + +DSPA("\n\n%s authenticator: using domain %s\n\n", ablock->name, domain); + +spa_build_auth_request(&request, CS username, domain); +spa_bits_to_base64(US msgbuf, US &request, spa_request_length(&request)); + +DSPA("\n\n%s authenticator: sending request (%s)\n\n", ablock->name, msgbuf); + +/* send the encrypted password */ +if (smtp_write_command(sx, SCMD_FLUSH, "%s\r\n", msgbuf) < 0) + return FAIL_SEND; + +/* wait for the auth challenge */ +if (!smtp_read_response(sx, US buffer, buffsize, '3', timeout)) + return FAIL; + +/* convert the challenge into the challenge struct */ +DSPA("\n\n%s authenticator: challenge (%s)\n\n", ablock->name, buffer + 4); +spa_base64_to_bits(CS (&challenge), sizeof(challenge), CCS (buffer + 4)); + +spa_build_auth_response(&challenge, &response, CS username, CS password); +spa_bits_to_base64(US msgbuf, US &response, spa_request_length(&response)); +DSPA("\n\n%s authenticator: challenge response (%s)\n\n", ablock->name, msgbuf); + +/* send the challenge response */ +if (smtp_write_command(sx, SCMD_FLUSH, "%s\r\n", msgbuf) < 0) + return FAIL_SEND; + +/* If we receive a success response from the server, authentication +has succeeded. There may be more data to send, but is there any point +in provoking an error here? */ + +if (smtp_read_response(sx, US buffer, buffsize, '2', timeout)) + return OK; + +/* Not a success response. If errno != 0 there is some kind of transmission +error. Otherwise, check the response code in the buffer. If it starts with +'3', more data is expected. */ + +if (errno != 0 || buffer[0] != '3') + return FAIL; + +return FAIL; +} + +#endif /*!MACRO_PREDEF*/ +/* End of spa.c */ diff --git a/src/auths/spa.h b/src/auths/spa.h new file mode 100644 index 0000000..ca93469 --- /dev/null +++ b/src/auths/spa.h @@ -0,0 +1,38 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2009 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* This file, which provides support for Microsoft's Secure Password +Authentication, was contributed by Marc Prud'hommeaux. */ + + +#include "auth-spa.h" + +/* Private structure for the private options. */ + +typedef struct { + uschar *spa_username; + uschar *spa_password; + uschar *spa_domain; + uschar *spa_serverpassword; +} auth_spa_options_block; + +/* Data for reading the private options. */ + +extern optionlist auth_spa_options[]; +extern int auth_spa_options_count; + +/* Block containing default values. */ + +extern auth_spa_options_block auth_spa_option_defaults; + +/* The entry points for the mechanism */ + +extern void auth_spa_init(auth_instance *); +extern int auth_spa_server(auth_instance *, uschar *); +extern int auth_spa_client(auth_instance *, void *, int, uschar *, int); + +/* End of spa.h */ diff --git a/src/auths/tls.c b/src/auths/tls.c new file mode 100644 index 0000000..325e7b4 --- /dev/null +++ b/src/auths/tls.c @@ -0,0 +1,94 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) Jeremy Harris 1995 - 2020 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* This file provides an Exim authenticator driver for +a server to verify a client SSL certificate +*/ + + +#include "../exim.h" +#include "tls.h" + +/* Options specific to the tls authentication mechanism. */ + +optionlist auth_tls_options[] = { + { "server_param", opt_stringptr, + OPT_OFF(auth_tls_options_block, server_param1) }, + { "server_param1", opt_stringptr, + OPT_OFF(auth_tls_options_block, server_param1) }, + { "server_param2", opt_stringptr, + OPT_OFF(auth_tls_options_block, server_param2) }, + { "server_param3", opt_stringptr, + OPT_OFF(auth_tls_options_block, server_param3) }, +}; + +/* Size of the options list. An extern variable has to be used so that its +address can appear in the tables drtables.c. */ + +int auth_tls_options_count = nelem(auth_tls_options); + +/* Default private options block for the authentication method. */ + +auth_tls_options_block auth_tls_option_defaults = { + NULL, /* server_param1 */ + NULL, /* server_param2 */ + NULL, /* server_param3 */ +}; + + +#ifdef MACRO_PREDEF + +/* Dummy values */ +void auth_tls_init(auth_instance *ablock) {} +int auth_tls_server(auth_instance *ablock, uschar *data) {return 0;} +int auth_tls_client(auth_instance *ablock, void * sx, + int timeout, uschar *buffer, int buffsize) {return 0;} + +#else /*!MACRO_PREDEF*/ + + + + +/************************************************* +* Initialization entry point * +*************************************************/ + +/* Called for each instance, after its options have been read, to +enable consistency checks to be done, or anything else that needs +to be set up. */ + +void +auth_tls_init(auth_instance *ablock) +{ +ablock->public_name = ablock->name; /* needed for core code */ +} + + + +/************************************************* +* Server entry point * +*************************************************/ + +/* For interface, see auths/README */ + +int +auth_tls_server(auth_instance *ablock, uschar *data) +{ +auth_tls_options_block * ob = (auth_tls_options_block *)ablock->options_block; + +if (ob->server_param1) + auth_vars[expand_nmax++] = expand_string(ob->server_param1); +if (ob->server_param2) + auth_vars[expand_nmax++] = expand_string(ob->server_param2); +if (ob->server_param3) + auth_vars[expand_nmax++] = expand_string(ob->server_param3); +return auth_check_serv_cond(ablock); +} + + +#endif /*!MACRO_PREDEF*/ +/* End of tls.c */ diff --git a/src/auths/tls.h b/src/auths/tls.h new file mode 100644 index 0000000..7aa95b6 --- /dev/null +++ b/src/auths/tls.h @@ -0,0 +1,30 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) Jeremy Harris 2015 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Private structure for the private options. */ + +typedef struct { + uschar * server_param1; + uschar * server_param2; + uschar * server_param3; +} auth_tls_options_block; + +/* Data for reading the private options. */ + +extern optionlist auth_tls_options[]; +extern int auth_tls_options_count; + +/* Block containing default values. */ + +extern auth_tls_options_block auth_tls_option_defaults; + +/* The entry points for the mechanism */ + +extern void auth_tls_init(auth_instance *); +extern int auth_tls_server(auth_instance *, uschar *); + +/* End of tls.h */ diff --git a/src/auths/xtextdecode.c b/src/auths/xtextdecode.c new file mode 100644 index 0000000..746dfbd --- /dev/null +++ b/src/auths/xtextdecode.c @@ -0,0 +1,58 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2022 */ +/* Copyright (c) University of Cambridge 1995 - 2009 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" + + +/************************************************* +* Decode byte-string in xtext * +*************************************************/ + +/* This function decodes a string in xtextformat as defined in RFC 1891 and +required by the SMTP AUTH extension (RFC 2554). We put the result in a piece of +store of equal length - it cannot be longer than this. Although in general the +result of decoding an xtext may be binary, in the context in which it is used +by Exim (for decoding the value of AUTH on a MAIL command), the result is +expected to be an addr-spec. We therefore add on a terminating zero, for +convenience. + +Arguments: + code points to the coded string, zero-terminated + ptr where to put the pointer to the result, which is in + dynamic store + +Returns: the number of bytes in the result, excluding the final zero; + -1 if the input is malformed +*/ + +int +auth_xtextdecode(uschar *code, uschar **ptr) +{ +register int x; +uschar * result = store_get(Ustrlen(code) + 1, code); +*ptr = result; + +while ((x = (*code++)) != 0) + { + if (x < 33 || x > 127 || x == '=') return -1; + if (x == '+') + { + register int y; + if (!isxdigit((x = (*code++)))) return -1; + y = ((isdigit(x))? x - '0' : (tolower(x) - 'a' + 10)) << 4; + if (!isxdigit((x = (*code++)))) return -1; + *result++ = y | ((isdigit(x))? x - '0' : (tolower(x) - 'a' + 10)); + } + else *result++ = x; + } + +*result = 0; +return result - *ptr; +} + +/* End of xtextdecode.c */ diff --git a/src/auths/xtextencode.c b/src/auths/xtextencode.c new file mode 100644 index 0000000..fc571c7 --- /dev/null +++ b/src/auths/xtextencode.c @@ -0,0 +1,58 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2022 */ +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "../exim.h" + + +/************************************************* +* Encode byte-string in xtext * +*************************************************/ + +/* This function encodes a string of bytes, containing any values whatsoever, +as "xtext", as defined in RFC 1891 and required by the SMTP AUTH extension (RFC +2554). + +Arguments: + clear points to the clear text bytes + len the number of bytes to encode + +Returns: a pointer to the zero-terminated xtext string, which + is in working store +*/ + +uschar * +auth_xtextencode(uschar *clear, int len) +{ +uschar *code; +uschar *p = US clear; +uschar *pp; +int c = len; +int count = 1; +register int x; + +/* We have to do a prepass to find out how many specials there are, +in order to get the right amount of store. */ + +while (c -- > 0) + count += ((x = *p++) < 33 || x > 127 || x == '+' || x == '=')? 3 : 1; + +pp = code = store_get(count, clear); + +p = US clear; +c = len; +while (c-- > 0) + if ((x = *p++) < 33 || x > 127 || x == '+' || x == '=') + pp += sprintf(CS pp, "+%.02x", x); /* There's always room */ + else + *pp++ = x; + +*pp = 0; +return code; +} + +/* End of xtextencode.c */ diff --git a/src/base64.c b/src/base64.c new file mode 100644 index 0000000..fa06a7a --- /dev/null +++ b/src/base64.c @@ -0,0 +1,297 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) Tom Kistner 2004, 2015 */ +/* License: GPL */ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +#include "exim.h" +#ifdef WITH_CONTENT_SCAN /* file-IO specific decode function */ +# include "mime.h" + +/* BASE64 decoder matrix */ +static unsigned char mime_b64[256]={ +/* 0 */ 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +/* 16 */ 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +/* 32 */ 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 62, 128, 128, 128, 63, +/* 48 */ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 128, 128, 128, 255, 128, 128, +/* 64 */ 128, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, +/* 80 */ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 128, 128, 128, 128, 128, +/* 96 */ 128, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, +/* 112 */ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 128, 128, 128, 128, 128, +/* 128 */ 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +/* 144 */ 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +/* 160 */ 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +/* 176 */ 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +/* 192 */ 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +/* 208 */ 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +/* 224 */ 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, +/* 240 */ 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128 +}; + +/* decode base64 MIME part */ +ssize_t +mime_decode_base64(FILE * in, FILE * out, uschar * boundary) +{ +uschar ibuf[MIME_MAX_LINE_LENGTH], obuf[MIME_MAX_LINE_LENGTH]; +uschar *opos; +ssize_t len, size = 0; +int bytestate = 0; + +opos = obuf; + +while (Ufgets(ibuf, MIME_MAX_LINE_LENGTH, in) != NULL) + { + if (boundary != NULL + && Ustrncmp(ibuf, "--", 2) == 0 + && Ustrncmp((ibuf+2), boundary, Ustrlen(boundary)) == 0 + ) + break; + + for (uschar * ipos = ibuf ; *ipos != '\r' && *ipos != '\n' && *ipos; ++ipos) + if (*ipos == '=') /* skip padding */ + ++bytestate; + + else if (mime_b64[*ipos] == 128) /* skip bad characters */ + mime_set_anomaly(MIME_ANOMALY_BROKEN_BASE64); + + /* simple state-machine */ + else switch((bytestate++) & 3) + { + case 0: + *opos = mime_b64[*ipos] << 2; break; + case 1: + *opos++ |= mime_b64[*ipos] >> 4; + *opos = mime_b64[*ipos] << 4; break; + case 2: + *opos++ |= mime_b64[*ipos] >> 2; + *opos = mime_b64[*ipos] << 6; break; + case 3: + *opos++ |= mime_b64[*ipos]; break; + } + + /* something to write? */ + len = opos - obuf; + if (len > 0) + { + if (fwrite(obuf, 1, len, out) != len) return -1; /* error */ + size += len; + /* copy incomplete last byte to start of obuf, where we continue */ + if ((bytestate & 3) != 0) + *obuf = *opos; + opos = obuf; + } + } /* while */ + +/* write out last byte if it was incomplete */ +if (bytestate & 3) + { + if (fwrite(obuf, 1, 1, out) != 1) return -1; + ++size; + } + +return size; +} + +#endif /*WITH_CONTENT_SCAN*/ + +/************************************************* + ************************************************* + ************************************************* + ************************************************* + ************************************************* + ************************************************* + ************************************************* + ************************************************* + ************************************************* + ************************************************* + ************************************************* + ************************************************* + ************************************************* + ************************************************* + ************************************************* + *************************************************/ + + +/************************************************* +* Decode byte-string in base 64 * +*************************************************/ + +/* This function decodes a string in base 64 format as defined in RFC 2045 +(MIME) and required by the SMTP AUTH extension (RFC 2554). The decoding +algorithm is written out in a straightforward way. Turning it into some kind of +compact loop is messy and would probably run more slowly. + +Arguments: + code points to the coded string, zero-terminated + ptr where to put the pointer to the result, which is in + allocated store, and zero-terminated + +Returns: the number of bytes in the result, + or -1 if the input was malformed + +Whitespace in the input is ignored. +A zero is added on to the end to make it easy in cases where the result is to +be interpreted as text. This is not included in the count. */ + +static uschar dec64table[] = { + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, /* 0-15 */ + 255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, /* 16-31 */ + 255,255,255,255,255,255,255,255,255,255,255, 62,255,255,255, 63, /* 32-47 */ + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61,255,255,255,255,255,255, /* 48-63 */ + 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, /* 64-79 */ + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,255,255,255,255,255, /* 80-95 */ + 255, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, /* 96-111 */ + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51,255,255,255,255,255 /* 112-127*/ +}; + +int +b64decode(const uschar *code, uschar **ptr) +{ + +int x, y; +uschar *result; + + { + int l = Ustrlen(code); + *ptr = result = store_get(1 + l/4 * 3 + l%4, code); + } + +/* Each cycle of the loop handles a quantum of 4 input bytes. For the last +quantum this may decode to 1, 2, or 3 output bytes. */ + +while ((x = *code++) != 0) + { + if (isspace(x)) continue; + /* debug_printf("b64d: '%c'\n", x); */ + + if (x > 127 || (x = dec64table[x]) == 255) return -1; + + while (isspace(y = *code++)) ; + /* debug_printf("b64d: '%c'\n", y); */ + if (y > 127 || (y = dec64table[y]) == 255) + return -1; + + *result++ = (x << 2) | (y >> 4); + /* debug_printf("b64d: -> %02x\n", result[-1]); */ + + while (isspace(x = *code++)) ; + /* debug_printf("b64d: '%c'\n", x); */ + if (x == '=') /* endmarker, but there should be another */ + { + while (isspace(x = *code++)) ; + /* debug_printf("b64d: '%c'\n", x); */ + if (x != '=') return -1; + while (isspace(y = *code++)) ; + if (y != 0) return -1; + /* debug_printf("b64d: DONE\n"); */ + break; + } + else + { + if (x > 127 || (x = dec64table[x]) == 255) return -1; + *result++ = (y << 4) | (x >> 2); + /* debug_printf("b64d: -> %02x\n", result[-1]); */ + + while (isspace(y = *code++)) ; + /* debug_printf("b64d: '%c'\n", y); */ + if (y == '=') + { + while (isspace(y = *code++)) ; + if (y != 0) return -1; + /* debug_printf("b64d: DONE\n"); */ + break; + } + else + { + if (y > 127 || (y = dec64table[y]) == 255) return -1; + *result++ = (x << 6) | y; + /* debug_printf("b64d: -> %02x\n", result[-1]); */ + } + } + } + +*result = 0; +return result - *ptr; +} + + +/************************************************* +* Encode byte-string in base 64 * +*************************************************/ + +/* This function encodes a string of bytes, containing any values whatsoever, +in base 64 as defined in RFC 2045 (MIME) and required by the SMTP AUTH +extension (RFC 2554). The encoding algorithm is written out in a +straightforward way. Turning it into some kind of compact loop is messy and +would probably run more slowly. + +Arguments: + clear points to the clear text bytes + len the number of bytes to encode + proto_mem taint indicator + +Returns: a pointer to the zero-terminated base 64 string, which + is in working store +*/ + +static uschar *enc64table = + US"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +uschar * +b64encode_taint(const uschar * clear, int len, const void * proto_mem) +{ +uschar * code = store_get(4*((len+2)/3) + 1, proto_mem); +uschar * p = code; + +while (len-- >0) + { + int x, y; + + x = *clear++; + *p++ = enc64table[(x >> 2) & 63]; + + if (len-- <= 0) + { + *p++ = enc64table[(x << 4) & 63]; + *p++ = '='; + *p++ = '='; + break; + } + + y = *clear++; + *p++ = enc64table[((x << 4) | ((y >> 4) & 15)) & 63]; + + if (len-- <= 0) + { + *p++ = enc64table[(y << 2) & 63]; + *p++ = '='; + break; + } + + x = *clear++; + *p++ = enc64table[((y << 2) | ((x >> 6) & 3)) & 63]; + + *p++ = enc64table[x & 63]; + } + +*p = 0; + +return code; +} + +uschar * +b64encode(const uschar * clear, int len) +{ +return b64encode_taint(clear, len, clear); +} + + +/* End of base64.c */ +/* vi: sw ai sw=2 +*/ diff --git a/src/blob.h b/src/blob.h new file mode 100644 index 0000000..a3f1e24 --- /dev/null +++ b/src/blob.h @@ -0,0 +1,15 @@ +/* + * Blob - a general pointer/size item for a memory chunk + * + * Copyright (C) 2016 Exim maintainers + */ + +#ifndef BLOB_H /* entire file */ +#define BLOB_H + +typedef struct { + uschar * data; + size_t len; +} blob; + +#endif diff --git a/src/bmi_spam.c b/src/bmi_spam.c new file mode 100644 index 0000000..af4bc46 --- /dev/null +++ b/src/bmi_spam.c @@ -0,0 +1,476 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Code for calling Brightmail AntiSpam. + Copyright (c) Tom Kistner 2004 + License: GPL */ +/* Copyright (c) The Exim Maintainers 2021 - 2022 */ + +#include "exim.h" +#ifdef EXPERIMENTAL_BRIGHTMAIL + +#include "bmi_spam.h" + +uschar *bmi_current_optin = NULL; + +uschar *bmi_process_message(header_line *header_list, int data_fd) { + BmiSystem *system = NULL; + BmiMessage *message = NULL; + BmiError err; + BmiErrorLocation err_loc; + BmiErrorType err_type; + const BmiVerdict *verdict = NULL; + FILE *data_file; + uschar data_buffer[4096]; + uschar localhost[] = "127.0.0.1"; + uschar *host_address; + uschar *verdicts = NULL; + int i,j; + + err = bmiInitSystem(BMI_VERSION, CS bmi_config_file, &system); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: could not initialize Brightmail system.", (int)err_loc, (int)err_type); + return NULL; + } + + err = bmiInitMessage(system, &message); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: could not initialize Brightmail message.", (int)err_loc, (int)err_type); + bmiFreeSystem(system); + return NULL; + } + + /* Send IP address of sending host */ + if (sender_host_address == NULL) + host_address = localhost; + else + host_address = sender_host_address; + err = bmiProcessConnection(CS host_address, message); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: bmiProcessConnection() failed (IP %s).", (int)err_loc, (int)err_type, CS host_address); + bmiFreeMessage(message); + bmiFreeSystem(system); + return NULL; + }; + + /* Send envelope sender address */ + err = bmiProcessFROM(CS sender_address, message); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: bmiProcessFROM() failed (address %s).", (int)err_loc, (int)err_type, CS sender_address); + bmiFreeMessage(message); + bmiFreeSystem(system); + return NULL; + }; + + /* Send envelope recipients */ + for(i=0;ibmi_optin != NULL) && (Ustrlen(r->bmi_optin) > 1)) { + debug_printf("passing bmiOptin string: %s\n", r->bmi_optin); + bmiOptinInit(&optin); + err = bmiOptinMset(optin, r->bmi_optin, ':'); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + log_write(0, LOG_PANIC|LOG_MAIN, + "bmi warning: [loc %d type %d]: bmiOptinMSet() failed (address '%s', string '%s').", (int)err_loc, (int)err_type, CS r->address, CS r->bmi_optin); + if (optin != NULL) + bmiOptinFree(optin); + optin = NULL; + }; + }; + + err = bmiAccumulateTO(CS r->address, optin, message); + + if (optin != NULL) + bmiOptinFree(optin); + + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: bmiAccumulateTO() failed (address %s).", (int)err_loc, (int)err_type, CS r->address); + bmiFreeMessage(message); + bmiFreeSystem(system); + return NULL; + }; + }; + err = bmiEndTO(message); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: bmiEndTO() failed.", (int)err_loc, (int)err_type); + bmiFreeMessage(message); + bmiFreeSystem(system); + return NULL; + }; + + /* Send message headers */ + while (header_list != NULL) { + /* skip deleted headers */ + if (header_list->type == '*') { + header_list = header_list->next; + continue; + }; + err = bmiAccumulateHeaders(CCS header_list->text, header_list->slen, message); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: bmiAccumulateHeaders() failed.", (int)err_loc, (int)err_type); + bmiFreeMessage(message); + bmiFreeSystem(system); + return NULL; + }; + header_list = header_list->next; + }; + err = bmiEndHeaders(message); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: bmiEndHeaders() failed.", (int)err_loc, (int)err_type); + bmiFreeMessage(message); + bmiFreeSystem(system); + return NULL; + }; + + /* Send body */ + data_file = fdopen(data_fd,"r"); + do { + j = fread(data_buffer, 1, sizeof(data_buffer), data_file); + if (j > 0) { + err = bmiAccumulateBody(CCS data_buffer, j, message); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: bmiAccumulateBody() failed.", (int)err_loc, (int)err_type); + bmiFreeMessage(message); + bmiFreeSystem(system); + return NULL; + }; + }; + } while (j > 0); + err = bmiEndBody(message); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: bmiEndBody() failed.", (int)err_loc, (int)err_type); + bmiFreeMessage(message); + bmiFreeSystem(system); + return NULL; + }; + + + /* End message */ + err = bmiEndMessage(message); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: bmiEndMessage() failed.", (int)err_loc, (int)err_type); + bmiFreeMessage(message); + bmiFreeSystem(system); + return NULL; + }; + + /* Get store for the verdict string. Since we are processing message data, assume that + the verdict is tainted. XXX this should use a growable-string */ + + verdicts = store_get(1, GET_TAINTED); + *verdicts = '\0'; + + for ( err = bmiAccessFirstVerdict(message, &verdict); + verdict; + err = bmiAccessNextVerdict(message, verdict, &verdict) ) { + char *verdict_str; + + err = bmiCreateStrFromVerdict(verdict,&verdict_str); + if (!store_extend(verdicts, + Ustrlen(verdicts)+1, Ustrlen(verdicts)+1+strlen(verdict_str)+1)) { + /* can't allocate more store */ + return NULL; + }; + if (*verdicts != '\0') + Ustrcat(verdicts, US ":"); + Ustrcat(verdicts, US verdict_str); + bmiFreeStr(verdict_str); + }; + + DEBUG(D_receive) debug_printf("bmi verdicts: %s\n", verdicts); + + if (Ustrlen(verdicts) == 0) + return NULL; + else + return verdicts; +} + + +int bmi_get_delivery_status(uschar *base64_verdict) { + BmiError err; + BmiErrorLocation err_loc; + BmiErrorType err_type; + BmiVerdict *verdict = NULL; + int rc = 1; /* deliver by default */ + + /* always deliver when there is no verdict */ + if (base64_verdict == NULL) + return 1; + + /* create verdict from base64 string */ + err = bmiCreateVerdictFromStr(CS base64_verdict, &verdict); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: bmiCreateVerdictFromStr() failed. [%s]", (int)err_loc, (int)err_type, base64_verdict); + return 1; + }; + + err = bmiVerdictError(verdict); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + /* deliver normally due to error */ + rc = 1; + } + else if (bmiVerdictDestinationIsDefault(verdict) == BMI_TRUE) { + /* deliver normally */ + rc = 1; + } + else if (bmiVerdictAccessDestination(verdict) == NULL) { + /* do not deliver */ + rc = 0; + } + else { + /* deliver to alternate location */ + rc = 1; + }; + + bmiFreeVerdict(verdict); + return rc; +} + + +uschar *bmi_get_alt_location(uschar *base64_verdict) { + BmiError err; + BmiErrorLocation err_loc; + BmiErrorType err_type; + BmiVerdict *verdict = NULL; + uschar *rc = NULL; + + /* always deliver when there is no verdict */ + if (base64_verdict == NULL) + return NULL; + + /* create verdict from base64 string */ + err = bmiCreateVerdictFromStr(CS base64_verdict, &verdict); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: bmiCreateVerdictFromStr() failed. [%s]", (int)err_loc, (int)err_type, base64_verdict); + return NULL; + }; + + err = bmiVerdictError(verdict); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + /* deliver normally due to error */ + rc = NULL; + } + else if (bmiVerdictDestinationIsDefault(verdict) == BMI_TRUE) { + /* deliver normally */ + rc = NULL; + } + else if (bmiVerdictAccessDestination(verdict) == NULL) { + /* do not deliver */ + rc = NULL; + } + else { + /* deliver to alternate location */ + rc = store_get(strlen(bmiVerdictAccessDestination(verdict))+1, GET_TAINTED); + Ustrcpy(rc, bmiVerdictAccessDestination(verdict)); + rc[strlen(bmiVerdictAccessDestination(verdict))] = '\0'; + }; + + bmiFreeVerdict(verdict); + return rc; +} + +uschar *bmi_get_base64_verdict(uschar *bmi_local_part, uschar *bmi_domain) { + BmiError err; + BmiErrorLocation err_loc; + BmiErrorType err_type; + BmiVerdict *verdict = NULL; + const BmiRecipient *recipient = NULL; + const char *verdict_str = NULL; + uschar *verdict_ptr; + uschar *verdict_buffer = NULL; + int sep = 0; + + /* return nothing if there are no verdicts available */ + if (bmi_verdicts == NULL) + return NULL; + + /* allocate room for the b64 verdict string */ + verdict_buffer = store_get(Ustrlen(bmi_verdicts)+1, GET_TAINTED); + + /* loop through verdicts */ + verdict_ptr = bmi_verdicts; + while ((verdict_str = CCS string_nextinlist(&verdict_ptr, &sep, + verdict_buffer, + Ustrlen(bmi_verdicts)+1)) != NULL) { + + /* create verdict from base64 string */ + err = bmiCreateVerdictFromStr(verdict_str, &verdict); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: bmiCreateVerdictFromStr() failed. [%s]", (int)err_loc, (int)err_type, verdict_str); + return NULL; + }; + + /* loop through rcpts for this verdict */ + for ( recipient = bmiVerdictAccessFirstRecipient(verdict); + recipient != NULL; + recipient = bmiVerdictAccessNextRecipient(verdict, recipient)) { + uschar *rcpt_local_part; + uschar *rcpt_domain; + + /* compare address against our subject */ + rcpt_local_part = US bmiRecipientAccessAddress(recipient); + rcpt_domain = Ustrchr(rcpt_local_part,'@'); + if (rcpt_domain == NULL) { + rcpt_domain = US""; + } + else { + *rcpt_domain = '\0'; + rcpt_domain++; + }; + + if ( (strcmpic(rcpt_local_part, bmi_local_part) == 0) && + (strcmpic(rcpt_domain, bmi_domain) == 0) ) { + /* found verdict */ + bmiFreeVerdict(verdict); + return US verdict_str; + }; + }; + + bmiFreeVerdict(verdict); + }; + + return NULL; +} + + +uschar *bmi_get_base64_tracker_verdict(uschar *base64_verdict) { + BmiError err; + BmiErrorLocation err_loc; + BmiErrorType err_type; + BmiVerdict *verdict = NULL; + uschar *rc = NULL; + + /* always deliver when there is no verdict */ + if (base64_verdict == NULL) + return NULL; + + /* create verdict from base64 string */ + err = bmiCreateVerdictFromStr(CS base64_verdict, &verdict); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: bmiCreateVerdictFromStr() failed. [%s]", (int)err_loc, (int)err_type, base64_verdict); + return NULL; + }; + + /* create old tracker string from verdict */ + err = bmiCreateOldStrFromVerdict(verdict, &rc); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: bmiCreateOldStrFromVerdict() failed. [%s]", (int)err_loc, (int)err_type, base64_verdict); + return NULL; + }; + + bmiFreeVerdict(verdict); + return rc; +} + + +int bmi_check_rule(uschar *base64_verdict, uschar *option_list) { + BmiError err; + BmiErrorLocation err_loc; + BmiErrorType err_type; + BmiVerdict *verdict = NULL; + int rc = 0; + uschar *rule_num; + uschar *rule_ptr; + uschar rule_buffer[32]; + int sep = 0; + + + /* no verdict -> no rule fired */ + if (base64_verdict == NULL) + return 0; + + /* create verdict from base64 string */ + err = bmiCreateVerdictFromStr(CS base64_verdict, &verdict); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + err_loc = bmiErrorGetLocation(err); + err_type = bmiErrorGetType(err); + log_write(0, LOG_PANIC, + "bmi error [loc %d type %d]: bmiCreateVerdictFromStr() failed. [%s]", (int)err_loc, (int)err_type, base64_verdict); + return 0; + }; + + err = bmiVerdictError(verdict); + if (bmiErrorIsFatal(err) == BMI_TRUE) { + /* error -> no rule fired */ + bmiFreeVerdict(verdict); + return 0; + } + + /* loop through numbers */ + /* option_list doesn't seem to be expanded so cannot be tainted. If it ever is we + will trap here */ + rule_ptr = option_list; + while ((rule_num = string_nextinlist(&rule_ptr, &sep, + rule_buffer, sizeof(rule_buffer)))) { + int rule_int = -1; + + /* try to translate to int */ + (void)sscanf(rule_num, "%d", &rule_int); + if (rule_int > 0) { + debug_printf("checking rule #%d\n", rule_int); + /* check if rule fired on the message */ + if (bmiVerdictRuleFired(verdict, rule_int) == BMI_TRUE) { + debug_printf("rule #%d fired\n", rule_int); + rc = 1; + break; + }; + }; + }; + + bmiFreeVerdict(verdict); + return rc; +}; + +#endif diff --git a/src/bmi_spam.h b/src/bmi_spam.h new file mode 100644 index 0000000..a9af778 --- /dev/null +++ b/src/bmi_spam.h @@ -0,0 +1,22 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Code for calling Brightmail AntiSpam. + Copyright (c) Tom Kistner 2004 + License: GPL */ + +#ifdef EXPERIMENTAL_BRIGHTMAIL + +#include + +extern uschar *bmi_process_message(header_line *, int); +extern uschar *bmi_get_base64_verdict(uschar *, uschar *); +extern uschar *bmi_get_base64_tracker_verdict(uschar *); +extern int bmi_get_delivery_status(uschar *); +extern uschar *bmi_get_alt_location(uschar *); +extern int bmi_check_rule(uschar *,uschar *); + +extern uschar *bmi_current_optin; + +#endif diff --git a/src/buildconfig.c b/src/buildconfig.c new file mode 100644 index 0000000..f9a8feb --- /dev/null +++ b/src/buildconfig.c @@ -0,0 +1,984 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2022 */ +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +/************************************************* +* Build configuration header for Exim * +*************************************************/ + +/* This auxiliary program builds the file config.h by the following +process: + +First, it determines the size of off_t and time_t variables, and generates +macro code to define OFF_T_FMT and TIME_T_FMT as suitable formats, if they are +not already defined in the system-specific header file. + +Then it reads Makefile, looking for certain OS-specific definitions which it +uses to define some specific macros. Finally, it reads the defaults file +config.h.defaults. + +The defaults file contains normal C #define statements for various macros; if +the name of a macro is found in the environment, the environment value replaces +the default. If the default #define does not contain any value, then that macro +is not copied to the created file unless there is some value in the +environment. + +This program is compiled and run as part of the Make process and is not +normally called independently. */ + + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +typedef struct { + const char *name; + int *flag; +} have_item; + +typedef struct { + const char *name; + char *data; +} save_item; + +static const char *db_opts[] = { "", "USE_DB", "USE_GDBM", "USE_TDB", "USE_NDBM" }; + +static int have_ipv6 = 0; +static int have_iconv = 0; + +static char errno_quota[256]; +static char ostype[256]; +static char cc[256]; + +/* If any entry is an initial substring of another, the longer one must +appear first. */ + +static have_item have_list[] = { + { "HAVE_IPV6", &have_ipv6 }, + { "HAVE_ICONV", &have_iconv }, + { NULL, NULL} +}; + +static save_item save_list[] = { + { "ERRNO_QUOTA", errno_quota }, + { "OSTYPE", ostype }, + { "CC", cc }, + { NULL, NULL} +}; + + +/* Subroutine to check a string for precisely one instance of "%s". If not, +bomb out. */ + +void +check_percent_ess(char *value, char *name) +{ +int OK = 0; +char *p = strstr(value, "%s"); +if (p != NULL) OK = strstr(p+2, "%s") == NULL; +if (!OK) + { + printf("\n*** \"%s\" (%s) must contain precisely one occurrence of\n" + "*** \"%%s\". Please review your build-time configuration.\n\n/", value, + name); + exit(1); + } +} + + +/* Main program */ + +int +main(int argc, char **argv) +{ +off_t test_off_t = 0; +time_t test_time_t = 0; +ino_t test_ino_t; +#if ! (__STDC_VERSION__ >= 199901L) +size_t test_size_t = 0; +ssize_t test_ssize_t = 0; +unsigned long test_ulong_t = 0L; +unsigned int test_uint_t = 0; +#endif +long test_long_t = 0; +long long test_longlong_t = 0; +int test_int_t = 0; +FILE *base; +FILE *new; +int last_initial = 'A'; +int linecount = 0; +int have_auth = 0; +int in_local_makefile = 0; +int use_which_db = 0; +int use_which_db_in_local_makefile = 0; +int support_crypteq = 0; +char buffer[1024]; + +if (argc != 1) + { + printf("*** Buildconfig: called with incorrect arguments\n"); + exit(1); + } + +new = fopen("config.h", "wb"); +if (new == NULL) + { + printf("*** Buildconfig: failed to open config.h for output\n"); + exit(1); + } + +printf("Building configuration file config.h\n"); + +fprintf(new, "/*************************************************\n"); +fprintf(new, "* Configuration header for Exim *\n"); +fprintf(new, "*************************************************/\n\n"); + +fprintf(new, "/* This file was automatically generated from Makefile and " + "config.h.defaults,\n"); +fprintf(new, "using values specified in the configuration file Local/Makefile.\n"); +fprintf(new, "Do not edit it. Instead, edit Local/Makefile and " + "rerun make. */\n\n"); + +/* First, deal with the printing format for off_t variables. We assume that if +the size of off_t is greater than 4, "%lld" will be available as a format for +printing long long variables, and there will be support for the long long type. +This assumption is known to be OK for the common operating systems. */ + +fprintf(new, "#ifndef OFF_T_FMT\n"); +if (sizeof(test_off_t) > sizeof(test_long_t)) + fprintf(new, "# define OFF_T_FMT \"%%lld\"\n"); +else + fprintf(new, "# define OFF_T_FMT \"%%ld\"\n"); +fprintf(new, "#endif\n\n"); + +fprintf(new, "#ifndef LONGLONG_T\n"); +if (sizeof(test_longlong_t) > sizeof(test_long_t)) + fprintf(new, "# define LONGLONG_T long long int\n"); +else + fprintf(new, "# define LONGLONG_T long int\n"); +fprintf(new, "#endif\n\n"); + +/* Now do the same thing for time_t variables. If the length is greater than +4, we want to assume long long support (even if off_t was less than 4). If the +length is 4 or less, we can leave LONGLONG_T to whatever was defined above for +off_t. */ + +fprintf(new, "#ifndef TIME_T_FMT\n"); +if (sizeof(test_time_t) > sizeof(test_long_t)) + { + fprintf(new, "# define TIME_T_FMT \"%%lld\"\n"); + fprintf(new, "# undef LONGLONG_T\n"); + fprintf(new, "# define LONGLONG_T long long int\n"); + } +else + fprintf(new, "# define TIME_T_FMT \"%%ld\"\n"); +fprintf(new, "#endif\n\n"); + +fprintf(new, "#ifndef INO_T_FMT\n"); +if (sizeof(test_ino_t) > sizeof(test_long_t)) + fprintf(new, "# define INO_T_FMT \"%%llu\"\n"); +else + fprintf(new, "# define INO_T_FMT \"%%lu\"\n"); +fprintf(new, "#endif\n\n"); + +fprintf(new, "#ifndef PID_T_FMT\n"); +fprintf(new, "# define PID_T_FMT \"%%lu\"\n"); +fprintf(new, "#endif\n\n"); + +/* And for sizeof() results, size_t, which should with C99 be just %zu, deal +with C99 not being ubiquitous yet. Unfortunately. Assume ssize_t is same +size as size_t on C99; if someone comes up with a version where it's not, fix +it then. */ + +#if __STDC_VERSION__ >= 199901L +fprintf(new, "#define SIZE_T_FMT \"%%zu\"\n"); +fprintf(new, "#define SSIZE_T_FMT \"%%zd\"\n"); +#else +if (sizeof(test_size_t) > sizeof (test_ulong_t)) + fprintf(new, "#define SIZE_T_FMT \"%%llu\"\n"); +else if (sizeof(test_size_t) > sizeof (test_uint_t)) + fprintf(new, "#define SIZE_T_FMT \"%%lu\"\n"); +else + fprintf(new, "#define SIZE_T_FMT \"%%u\"\n"); + +if (sizeof(test_ssize_t) > sizeof(test_long_t)) + fprintf(new, "#define SSIZE_T_FMT \"%%lld\"\n"); +else if (sizeof(test_ssize_t) > sizeof(test_int_t)) + fprintf(new, "#define SSIZE_T_FMT \"%%ld\"\n"); +else + fprintf(new, "#define SSIZE_T_FMT \"%%d\"\n"); +#endif + +/* Now search the makefile for certain settings */ + +if (!(base = fopen("Makefile", "rb"))) + { + printf("*** Buildconfig: failed to open Makefile\n"); + (void)fclose(new); + exit(1); + } + +errno_quota[0] = 0; /* no over-riding value set */ +ostype[0] = 0; /* just in case */ +cc[0] = 0; + +while (fgets(buffer, sizeof(buffer), base) != NULL) + { + int i; + have_item *h; + save_item *s; + char *p = buffer + (int)strlen(buffer); + linecount++; + while (p > buffer && isspace((unsigned char)p[-1])) p--; + *p = 0; + p = buffer; + while (isspace((unsigned char)*p)) p++; + + /* Notice when we hit the user's makefile */ + + if (strcmp(p, "# From Local/Makefile") == 0) + { + in_local_makefile = 1; + continue; + } + + /* Remember the last DB option setting. If we hit two in the user's + Makefile, complain. */ + + for (i = 1; i < sizeof(db_opts)/sizeof(char *); i++) + { + int len = (int)strlen(db_opts[i]); + if (strncmp(p, db_opts[i], len) == 0 && (p[len] == ' ' || p[len] == '=')) + { + if (in_local_makefile) + { + if (use_which_db_in_local_makefile) + { + printf("*** Only one of USE_DB, USE_GDBM, or USE_TDB should be " + "defined in Local/Makefile\n"); + exit(1); + } + use_which_db_in_local_makefile = 1; + } + use_which_db = i; + break; + } + } + if (i < sizeof(db_opts)/sizeof(char *)) continue; + + /* Items where we just save a boolean */ + + for (h = have_list; h->name != NULL; h++) + { + int len = (int)strlen(h->name); + if (strncmp(p, h->name, len) == 0) + { + p += len; + while (isspace((unsigned char)*p)) p++; + if (*p++ != '=') + { + printf("*** Buildconfig: syntax error in Makefile line %d\n", linecount); + exit(1); + } + while (isspace((unsigned char)*p)) p++; + if (strcmp(p, "YES") == 0 || strcmp(p, "yes") == 0) *(h->flag) = 1; + else *(h->flag) = 0; /* Must reset in case multiple instances */ + break; + } + } + + if (h->name != NULL) continue; + + /* Items where we save the complete string */ + + for (s = save_list; s->name != NULL; s++) + { + int len = (int)strlen(s->name); + if (strncmp(p, s->name, len) == 0) + { + p += len; + while (isspace((unsigned char)*p)) p++; + if (*p++ != '=') + { + printf("*** Buildconfig: syntax error in Makefile line %d\n", linecount); + exit(1); + } + while (isspace((unsigned char)*p)) p++; + strcpy(s->data, p); + } + } + } + +fprintf(new, "#define HAVE_IPV6 %s\n", + have_ipv6? "TRUE" : "FALSE"); + +fprintf(new, "#define HAVE_ICONV %s\n", + have_iconv? "TRUE" : "FALSE"); + +if (errno_quota[0] != 0) + fprintf(new, "\n#define ERRNO_QUOTA %s\n", errno_quota); + +if (strcmp(cc, "gcc") == 0 && + (strstr(ostype, "IRIX") != NULL || strstr(ostype, "AIX") != NULL)) + { + fprintf(new, "\n/* This switch includes the code to fix the inet_ntoa() */"); + fprintf(new, "\n/* bug when using gcc on an IRIX or AIX system. */"); + fprintf(new, "\n#define USE_INET_NTOA_FIX"); + } + +fprintf(new, "\n"); +(void)fclose(base); + + +/* Now handle the macros listed in the defaults */ + +base = fopen("../src/config.h.defaults", "rb"); +if (base == NULL) + { + printf("*** Buildconfig: failed to open ../src/config.h.defaults\n"); + (void)fclose(new); + exit(1); + } + +while (fgets(buffer, sizeof(buffer), base) != NULL) + { + int i; + char name[256]; + char *value; + char *p = buffer; + char *q = name; + + while (*p == ' ' || *p == '\t') p++; + + if (strncmp(p, "#ifdef ", 7) == 0 + || strncmp(p, "#ifndef ", 8) == 0 + || strncmp(p, "#if ", 4) == 0 + || strncmp(p, "#endif", 6) == 0 + ) + { + fputs(buffer, new); + continue; + } + + if (strncmp(p, "#define ", 8) != 0) continue; + + p += 8; + while (*p == ' ' || *p == '\t') p++; + + if (*p < last_initial) fprintf(new, "\n"); + last_initial = *p; + + while (*p && (isalnum((unsigned char)*p) || *p == '_')) *q++ = *p++; + *q = 0; + + /* USE_DB, USE_GDBM, and USE_TDB are special cases. We want to have only + one of them set. The scan of the Makefile has saved which was the last one + encountered. */ + + for (i = 1; i < sizeof(db_opts)/sizeof(char *); i++) + if (strcmp(name, db_opts[i]) == 0) + { + if (use_which_db == i) + fprintf(new, "#define %s %.*syes\n", db_opts[i], + 21 - (int)strlen(db_opts[i]), " "); + else + fprintf(new, "/* %s not set */\n", name); + break; + } + if (i < sizeof(db_opts)/sizeof(char *)) continue; + + /* EXIM_USER is a special case. We look in the environment for EXIM_USER or + EXIM_UID (the latter for backward compatibility with Exim 3). If the value is + not numeric, we look up the user, and default the GID if found. Otherwise, + EXIM_GROUP or EXIM_GID must be in the environment. */ + + if (strcmp(name, "EXIM_UID") == 0) + { + uid_t uid = 0; + gid_t gid = 0; + int gid_set = 0; + int uid_not_set = 0; + char *username = NULL; + char *groupname = NULL; + char *s; + char *user = getenv("EXIM_USER"); + char *group = getenv("EXIM_GROUP"); + + if (user == NULL) user = getenv("EXIM_UID"); + if (group == NULL) group = getenv("EXIM_GID"); + + if (user == NULL) + { + printf("\n*** EXIM_USER has not been defined in any of the Makefiles in " + "the\n \"Local\" directory. Please review your build-time " + "configuration.\n\n"); + return 1; + } + + while (isspace((unsigned char)(*user))) user++; + if (*user == 0) + { + printf("\n*** EXIM_USER is defined as an empty string in one of the " + "files\n in the \"Local\" directory. Please review your build-time" + "\n configuration.\n\n"); + return 1; + } + + for (s = user; *s != 0; s++) + { + if (iscntrl((unsigned char)(*s))) + { + printf("\n*** EXIM_USER contains the control character 0x%02X in one " + "of the files\n in the \"Local\" directory. Please review your " + "build-time\n configuration.\n\n", *s); + return 1; + } + } + + /* Numeric uid given */ + + if (user[strspn(user, "0123456789")] == 0) + { + uid = (uid_t)atoi(user); + } + + /* User name given. Normally, we look up the uid right away. However, + people building binary distributions sometimes want to retain the name till + runtime. This is supported if the name begins "ref:". */ + + else if (strncmp(user, "ref:", 4) == 0) + { + user += 4; + while (isspace(*user)) user++; + username = user; + gid_set = 1; + uid_not_set = 1; + } + + else + { + struct passwd *pw = getpwnam(user); + if (pw == NULL) + { + printf("\n*** User \"%s\" (specified in one of the Makefiles) does not " + "exist.\n Please review your build-time configuration.\n\n", + user); + return 1; + } + + uid = pw->pw_uid; + gid = pw->pw_gid; + gid_set = 1; + } + + /* Use explicit group if set. */ + + if (group != NULL) + { + while (isspace((unsigned char)(*group))) group++; + if (*group == 0) + { + printf("\n*** EXIM_GROUP is defined as an empty string in one of " + "the files in the\n \"Local\" directory. "); + if (gid_set) + { + printf("If you want the Exim group to be taken from the\n " + "password data for the Exim user, just remove the EXIM_GROUP " + "setting.\n Otherwise, p"); + } + else printf("EXIM_USER is defined numerically, so there is no" + "\n default for EXIM_GROUP and you must set it explicitly.\n P"); + printf("lease review your build-time configuration.\n\n"); + return 1; + } + + for (s = group; *s != 0; s++) + { + if (iscntrl((unsigned char)(*s))) + { + printf("\n*** EXIM_GROUP contains the control character 0x%02X in one " + "of the files\n in the \"Local\" directory. Please review your " + "build-time\n configuration.\n\n", *s); + return 1; + } + } + + /* Group name given. This may be by reference or to be looked up now, + as for user. */ + + if (strncmp(group, "ref:", 4) == 0) + { + group += 4; + while (isspace(*group)) group++; + groupname = group; + } + + else if (username != NULL) + { + groupname = group; + } + + else if (group[strspn(group, "0123456789")] == 0) + { + gid = (gid_t)atoi(group); + } + + else + { + struct group *gr = getgrnam(group); + if (gr == NULL) + { + printf("\n*** Group \"%s\" (specified in one of the Makefiles) does " + "not exist.\n Please review your build-time configuration.\n\n", + group); + return 1; + } + gid = gr->gr_gid; + } + } + + /* Else trouble unless found in passwd file with user */ + + else if (!gid_set) + { + printf("\n*** No group set for Exim. Please review your build-time " + "configuration.\n\n"); + return 1; + } + + /* security sanity checks + if ref: is being used, we can never be sure, but we can take reasonable + steps to filter out the most obvious ones. */ + + if ((!uid_not_set && uid == 0) || + ((username != NULL) && ( + (strcmp(username, "root") == 0) || + (strcmp(username, "toor") == 0) ))) + { + printf("\n*** Exim's internal user must not be root.\n\n"); + return 1; + } + + /* Output user and group names or uid/gid. When names are set, uid/gid + are set to zero but will be replaced at runtime. */ + + if (username != NULL) + fprintf(new, "#define EXIM_USERNAME \"%s\"\n", username); + if (groupname != NULL) + fprintf(new, "#define EXIM_GROUPNAME \"%s\"\n", groupname); + + fprintf(new, "#define EXIM_UID %d\n", (int)uid); + fprintf(new, "#define EXIM_GID %d\n", (int)gid); + continue; + } + + /* CONFIGURE_OWNER and CONFIGURE_GROUP are special cases. We look in the + environment for first. If the value is not numeric, we look up the user or + group. A lot of this code is similar to that for EXIM_USER, but it's easier + to keep it separate. */ + + if (strcmp(name, "CONFIGURE_OWNER") == 0 || + strcmp(name, "CONFIGURE_GROUP") == 0) + { + int isgroup = name[10] == 'G'; + uid_t uid = 0; + gid_t gid = 0; + const char *s; + const char *username = NULL; + const char *user = getenv(name); + + if (user == NULL) user = ""; + while (isspace((unsigned char)(*user))) user++; + if (*user == 0) + { + fprintf(new, "/* %s not set */\n", name); + continue; + } + + for (s = user; *s != 0; s++) + { + if (iscntrl((unsigned char)(*s))) + { + printf("\n*** %s contains the control character 0x%02X in " + "one of the files\n in the \"Local\" directory. Please review " + "your build-time\n configuration.\n\n", name, *s); + return 1; + } + } + + /* Numeric uid given */ + + if (user[strspn(user, "0123456789")] == 0) + { + if (isgroup) + gid = (gid_t)atoi(user); + else + uid = (uid_t)atoi(user); + } + + /* Name given. Normally, we look up the uid or gid right away. However, + people building binary distributions sometimes want to retain the name till + runtime. This is supported if the name begins "ref:". */ + + else if (strncmp(user, "ref:", 4) == 0) + { + user += 4; + while (isspace(*user)) user++; + username = user; + } +else if (isgroup) + { + struct group *gr = getgrnam(user); + if (gr == NULL) + { + printf("\n*** Group \"%s\" (specified in one of the Makefiles) does not " + "exist.\n Please review your build-time configuration.\n\n", + user); + return 1; + } + gid = gr->gr_gid; + } + + else + { + struct passwd *pw = getpwnam(user); + if (pw == NULL) + { + printf("\n*** User \"%s\" (specified in one of the Makefiles) does not " + "exist.\n Please review your build-time configuration.\n\n", + user); + return 1; + } + uid = pw->pw_uid; + } + + /* Output user and group names or uid/gid. When names are set, uid/gid + are set to zero but will be replaced at runtime. */ + + if (username != NULL) + { + if (isgroup) + fprintf(new, "#define CONFIGURE_GROUPNAME \"%s\"\n", username); + else + fprintf(new, "#define CONFIGURE_OWNERNAME \"%s\"\n", username); + } + + if (isgroup) + fprintf(new, "#define CONFIGURE_GROUP %d\n", (int)gid); + else + fprintf(new, "#define CONFIGURE_OWNER %d\n", (int)uid); + continue; + } + + /* FIXED_NEVER_USERS is another special case. Look up the uid values and + create suitable initialization data for a vector. */ + + if (strcmp(name, "FIXED_NEVER_USERS") == 0) + { + char *list = getenv("FIXED_NEVER_USERS"); + if (list == NULL) + { + fprintf(new, "#define FIXED_NEVER_USERS 0\n"); + } + else + { + int count = 1; + int i, j; + uid_t *vector; + char *p = list; + while (*p != 0) if (*p++ == ':') count++; + + vector = malloc((count+1) * sizeof(uid_t)); + vector[0] = (uid_t)count; + + for (i = 1, j = 0; i <= count; list++, i++) + { + char name[64]; + + p = list; + while (*list != 0 && *list != ':') list++; + strncpy(name, p, list-p); + name[list-p] = 0; + + if (name[0] == 0) + { + continue; + } + else if (name[strspn(name, "0123456789")] == 0) + { + vector[j++] = (uid_t)atoi(name); + } + else + { + struct passwd *pw = getpwnam(name); + if (pw == NULL) + { + printf("\n*** User \"%s\" (specified for FIXED_NEVER_USERS in one of the Makefiles) does not " + "exist.\n Please review your build-time configuration.\n\n", + name); + return 1; + } + vector[j++] = pw->pw_uid; + } + } + fprintf(new, "#define FIXED_NEVER_USERS %d", j); + for (i = 0; i < j; i++) fprintf(new, ", %d", (unsigned int)vector[i]); + fprintf(new, "\n"); + free(vector); + } + continue; + } + + /* WITH_CONTENT_SCAN is another special case: it must be set if it or + EXPERIMENTAL_DCC is set. */ + + if (strcmp(name, "WITH_CONTENT_SCAN") == 0) + { + char *wcs = getenv("WITH_CONTENT_SCAN"); + char *dcc = getenv("EXPERIMENTAL_DCC"); + fprintf(new, wcs || dcc + ? "#define WITH_CONTENT_SCAN yes\n" + : "/* WITH_CONTENT_SCAN not set */\n"); + continue; + } + + /* DISABLE_DKIM is special; must be forced if DISABLE_TLS */ + if (strcmp(name, "DISABLE_DKIM") == 0) + { + char *d_dkim = getenv("DISABLE_DKIM"); + char *notls = getenv("DISABLE_TLS"); + + if (d_dkim) + fprintf(new, "#define DISABLE_DKIM yes\n"); + else if (notls) + fprintf(new, "#define DISABLE_DKIM yes /* forced by lack of TLS */\n"); + else + fprintf(new, "/* DISABLE_DKIM not set */\n"); + continue; + } + + /* Otherwise, check whether a value exists in the environment. Remember if + it is an AUTH setting or SUPPORT_CRYPTEQ. */ + + if ((value = getenv(name)) != NULL) + { + int len; + len = 21 - (int)strlen(name); + + if (strncmp(name, "AUTH_", 5) == 0) have_auth = 1; + if (strncmp(name, "SUPPORT_CRYPTEQ", 15) == 0) support_crypteq = 1; + + /* The text value of LDAP_LIB_TYPE refers to a macro that gets set. */ + + if (strcmp(name, "LDAP_LIB_TYPE") == 0) + { + if (strcmp(value, "NETSCAPE") == 0 || + strcmp(value, "UMICHIGAN") == 0 || + strcmp(value, "OPENLDAP1") == 0 || + strcmp(value, "OPENLDAP2") == 0 || + strcmp(value, "SOLARIS") == 0 || + strcmp(value, "SOLARIS7") == 0) /* Compatibility */ + { + fprintf(new, "#define LDAP_LIB_%s\n", value); + } + else + { + printf("\n*** LDAP_LIB_TYPE=%s is not a recognized LDAP library type." + "\n*** Please review your build-time configuration.\n\n", value); + return 1; + } + } + + else if (strcmp(name, "RADIUS_LIB_TYPE") == 0) + { + if (strcmp(value, "RADIUSCLIENT") == 0 || + strcmp(value, "RADIUSCLIENTNEW") == 0 || + strcmp(value, "RADLIB") == 0) + { + fprintf(new, "#define RADIUS_LIB_%s\n", value); + } + else + { + printf("\n*** RADIUS_LIB_TYPE=%s is not a recognized RADIUS library type." + "\n*** Please review your build-time configuration.\n\n", value); + return 1; + } + } + + /* Other macros get set to the environment value. */ + + else + { + fprintf(new, "#define %s ", name); + while(len-- > 0) fputc(' ', new); + + /* LOG_FILE_PATH is now messy because it can be a path containing %s or + it can be "syslog" or ":syslog" or "syslog:path" or even "path:syslog". */ + + if (strcmp(name, "LOG_FILE_PATH") == 0) + { + char *ss = value; + for(;;) + { + char *pp; + char *sss = strchr(ss, ':'); + if (sss != NULL) + { + strncpy(buffer, ss, sss-ss); + buffer[sss-ss] = 0; /* For empty case */ + } + else + { + strncpy(buffer, ss, sizeof(buffer)); + buffer[sizeof(buffer)-1] = 0; + } + pp = buffer + (int)strlen(buffer); + while (pp > buffer && isspace((unsigned char)pp[-1])) pp--; + *pp = 0; + if (buffer[0] != 0 && strcmp(buffer, "syslog") != 0) + check_percent_ess(buffer, name); + if (sss == NULL) break; + ss = sss + 1; + while (isspace((unsigned char)*ss)) ss++; + } + fprintf(new, "\"%s\"\n", value); + } + + /* Timezone values HEADERS_CHARSET, TCP_WRAPPERS_DAEMON_NAME and + WHITELIST_D_MACROS get quoted */ + + else if (strcmp(name, "TIMEZONE_DEFAULT") == 0|| + strcmp(name, "TCP_WRAPPERS_DAEMON_NAME") == 0|| + strcmp(name, "HEADERS_CHARSET") == 0|| + strcmp(name, "WHITELIST_D_MACROS") == 0) + fprintf(new, "\"%s\"\n", value); + + /* GnuTLS constants; first is for debugging, others are tuning */ + + /* less than 0 is not-active; 0-9 are normal, API suggests higher + taken without problems */ + else if (strcmp(name, "EXIM_GNUTLS_LIBRARY_LOG_LEVEL") == 0) + { + long nv; + char *end; + nv = strtol(value, &end, 10); + if (end != value && *end == '\0' && nv >= -1 && nv <= 100) + { + fprintf(new, "%s\n", value); + } + else + { + printf("Value of %s should be -1..9\n", name); + return 1; + } + } + + /* how many bits Exim, as a client, demands must be in D-H */ + /* 1024 is a historical figure; some sites actually use lower, so we + permit the value to be lowered "dangerously" low, but not "insanely" + low. Though actually, 1024 is becoming "dangerous". */ + else if ((strcmp(name, "EXIM_CLIENT_DH_MIN_MIN_BITS") == 0) || + (strcmp(name, "EXIM_CLIENT_DH_DEFAULT_MIN_BITS") == 0) || + (strcmp(name, "EXIM_SERVER_DH_BITS_PRE2_12") == 0)) + { + long nv; + char *end; + nv = strtol(value, &end, 10); + if (end != value && *end == '\0' && nv >= 512 && nv < 500000) + { + fprintf(new, "%s\n", value); + } + else + { + printf("Unreasonable value (%s) of \"%s\".\n", value, name); + return 1; + } + } + + /* For others, quote any paths and don't quote anything else */ + + else + { + if (value[0] == '/') fprintf(new, "\"%s\"\n", value); + else fprintf(new, "%s\n", value); + } + } + } + + /* Value not defined in the environment; use the default */ + + else + { + char *t = p; + while (*p == ' ' || *p == '\t') p++; + if (*p != '\n') fputs(buffer, new); else + { + *t = 0; + if (strcmp(name, "BIN_DIRECTORY") == 0 || + strcmp(name, "CONFIGURE_FILE") == 0) + { + printf("\n*** %s has not been defined in any of the Makefiles in the\n" + " \"Local\" directory. " + "Please review your build-time configuration.\n\n", name); + return 1; + } + + if (strcmp(name, "TIMEZONE_DEFAULT") == 0) + { + char *tz = getenv("TZ"); + fprintf(new, "#define TIMEZONE_DEFAULT "); + if (tz == NULL) fprintf(new, "NULL\n"); else + fprintf(new, "\"%s\"\n", tz); + } + + else fprintf(new, "/* %s not set */\n", name); + } + } + } + +(void)fclose(base); + +/* If any AUTH macros were defined, ensure that SUPPORT_CRYPTEQ is also +defined. */ + +if (have_auth) + if (!support_crypteq) fprintf(new, "/* Force SUPPORT_CRYPTEQ for AUTH */\n" + "#define SUPPORT_CRYPTEQ\n"); + +/* Check poll() for timer functionality. +Some OS' have released with it broken. */ + + { + struct timeval before, after; + size_t us; + + gettimeofday(&before, NULL); + (void) poll(NULL, 0, 500); + gettimeofday(&after, NULL); + + us = (after.tv_sec - before.tv_sec) * 1000000 + + (after.tv_usec - before.tv_usec); + + if (us < 400000) + fprintf(new, "#define NO_POLL_H\n"); + } + +/* End off */ + +fprintf(new, "\n/* End of config.h */\n"); +(void)fclose(new); +return 0; +} + +/* End of buildconfig.c */ diff --git a/src/child.c b/src/child.c new file mode 100644 index 0000000..1f38b58 --- /dev/null +++ b/src/child.c @@ -0,0 +1,556 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* Copyright (c) University of Cambridge 1995 - 2015 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +#include "exim.h" + +static void (*oldsignal)(int); + + +/************************************************* +* Ensure an fd has a given value * +*************************************************/ + +/* This function is called when we want to ensure that a certain fd has a +specific value (one of 0, 1, 2). If it hasn't got it already, close the value +we want, duplicate the fd, then close the old one. + +Arguments: + oldfd original fd + newfd the fd we want + +Returns: nothing +*/ + +void +force_fd(int oldfd, int newfd) +{ +if (oldfd == newfd) return; +(void)close(newfd); +(void)dup2(oldfd, newfd); +(void)close(oldfd); +} + + +#ifndef STAND_ALONE +/************************************************* +* Build argv list and optionally re-exec Exim * +*************************************************/ + +/* This function is called when Exim wants to re-exec (overlay) itself in the +current process. This is different to child_open_exim(), which runs another +Exim process in parallel (but it then calls this function). The function's +basic job is to build the argv list according to the values of current options +settings. There is a basic list that all calls require, and an additional list +that some do not require. Further additions can be given as additional +arguments. An option specifies whether the exec() is actually to happen, and if +so, what is to be done if it fails. + +Arguments: + exec_type CEE_RETURN_ARGV => don't exec; return the argv list + CEE_EXEC_EXIT => just exit() on exec failure + CEE_EXEC_PANIC => panic-die on exec failure + kill_v if TRUE, don't pass on the D_v flag + pcount if not NULL, points to extra size of argv required, and if + CEE_RETURN_ARGV is specified, it is updated to give the + number of slots used + minimal TRUE if only minimal argv is required + acount number of additional arguments + ... further values to add to argv + +Returns: if CEE_RETURN_ARGV is given, returns a pointer to argv; + otherwise, does not return +*/ + +uschar ** +child_exec_exim(int exec_type, BOOL kill_v, int *pcount, BOOL minimal, + int acount, ...) +{ +int first_special = -1; +int n = 0; +int extra = pcount ? *pcount : 0; +uschar **argv; + +argv = store_get((extra + acount + MAX_CLMACROS + 24) * sizeof(char *), GET_UNTAINTED); + +/* In all case, the list starts out with the path, any macros, and a changed +config file. */ + +argv[n++] = exim_path; /* assume untainted */ +if (clmacro_count > 0) + { + memcpy(argv + n, clmacros, clmacro_count * sizeof(uschar *)); + n += clmacro_count; + } +if (f.config_changed) + { argv[n++] = US"-C"; argv[n++] = config_main_filename; } + +/* These values are added only for non-minimal cases. If debug_selector is +precisely D_v, we have to assume this was started by a non-admin user, and +we suppress the flag when requested. (This happens when passing on an SMTP +connection, and after ETRN.) If there's more debugging going on, an admin user +was involved, so we do pass it on. */ + +if (!minimal) + { + if (debug_selector == D_v) + { + if (!kill_v) argv[n++] = US"-v"; + } + else + { + if (debug_selector != 0) + { + argv[n++] = string_sprintf("-d=0x%x", debug_selector); + if (debug_fd > 2) + { + int flags = fcntl(debug_fd, F_GETFD); + if (flags != -1) (void)fcntl(debug_fd, F_SETFD, flags & ~FD_CLOEXEC); + close(2); + dup2(debug_fd, 2); + close(debug_fd); + } + } + } + if (debug_pretrigger_buf) + { argv[n++] = US"-dp"; argv[n++] = string_sprintf("0x%x", debug_pretrigger_bsize); } + if (dtrigger_selector != 0) + argv[n++] = string_sprintf("-dt=0x%x", dtrigger_selector); + DEBUG(D_any) + { + argv[n++] = US"-MCd"; + argv[n++] = US process_purpose; + } + if (!f.testsuite_delays) argv[n++] = US"-odd"; + if (f.dont_deliver) argv[n++] = US"-N"; + if (f.queue_smtp) argv[n++] = US"-odqs"; + if (f.synchronous_delivery) argv[n++] = US"-odi"; + if (connection_max_messages >= 0) + argv[n++] = string_sprintf("-oB%d", connection_max_messages); + if (*queue_name) + { argv[n++] = US"-MCG"; argv[n++] = queue_name; } + } + +/* Now add in any others that are in the call. Remember which they were, +for more helpful diagnosis on failure. */ + +if (acount > 0) + { + va_list ap; + va_start(ap, acount); + first_special = n; + while (acount-- > 0) + argv[n++] = va_arg(ap, uschar *); + va_end(ap); + } + +/* Terminate the list, and return it, if that is what is wanted. */ + +argv[n] = NULL; +if (exec_type == CEE_RETURN_ARGV) + { + if (pcount) *pcount = n; + return argv; + } + +/* Otherwise, do the exec() here, and handle the consequences of an unexpected +failure. We know that there will always be at least one extra option in the +call when exec() is done here, so it can be used to add to the panic data. */ + +DEBUG(D_exec) debug_print_argv(CUSS argv); +exim_nullstd(); /* Make sure std{in,out,err} exist */ +execv(CS argv[0], (char *const *)argv); + +log_write(0, + LOG_MAIN | (exec_type == CEE_EXEC_EXIT ? LOG_PANIC : LOG_PANIC_DIE), + "re-exec of exim (%s) with %s failed: %s", exim_path, argv[first_special], + strerror(errno)); + +/* Get here if exec_type == CEE_EXEC_EXIT. +Note: this must be _exit(), not exit(). */ + +_exit(EX_EXECFAILED); + +return NULL; /* To keep compilers happy */ +} + + + + +/************************************************* +* Create a child Exim process * +*************************************************/ + +/* This function is called when Exim wants to run a parallel instance of itself +in order to inject a message via the standard input. The function creates a +child process and runs Exim in it. It sets up a pipe to the standard input of +the new process, and returns that to the caller via fdptr. The function returns +the pid of the new process, or -1 if things go wrong. If debug_fd is +non-negative, it is passed as stderr. + +This interface is now a just wrapper for the more complicated function +child_open_exim2(), which has additional arguments. The wrapper must continue +to exist, even if all calls from within Exim are changed, because it is +documented for use from local_scan(). + +Argument: fdptr pointer to int for the stdin fd + purpose of the child process, for debug +Returns: pid of the created process or -1 if anything has gone wrong +*/ + +pid_t +child_open_exim_function(int * fdptr, const uschar * purpose) +{ +return child_open_exim2_function(fdptr, US"<>", bounce_sender_authentication, + purpose); +} + + +/* This is a more complicated function for creating a child Exim process, with +more arguments. + +Arguments: + fdptr pointer to int for the stdin fd + sender for a sender address (data for -f) + sender_authentication authenticated sender address or NULL + purpose of the child process, for debug + +Returns: pid of the created process or -1 if anything has gone wrong +*/ + +pid_t +child_open_exim2_function(int * fdptr, uschar * sender, + uschar * sender_authentication, const uschar * purpose) +{ +int pfd[2]; +int save_errno; +pid_t pid; + +/* Create the pipe and fork the process. Ensure that SIGCHLD is set to +SIG_DFL before forking, so that the child process can be waited for. We +sometimes get here with it set otherwise. Save the old state for resetting +on the wait. */ + +if (pipe(pfd) != 0) return (pid_t)(-1); +oldsignal = signal(SIGCHLD, SIG_DFL); +pid = exim_fork(purpose); + +/* Child process: make the reading end of the pipe into the standard input and +close the writing end. If debugging, pass debug_fd as stderr. Then re-exec +Exim with appropriate options. In the test harness, use -odi unless queue_only +is set, so that the bounce is fully delivered before returning. Failure is +signalled with EX_EXECFAILED (specified by CEE_EXEC_EXIT), but this shouldn't +occur. */ + +if (pid == 0) + { + force_fd(pfd[pipe_read], 0); + (void)close(pfd[pipe_write]); + if (debug_fd > 0) force_fd(debug_fd, 2); + if (f.running_in_test_harness && !queue_only) + { + if (sender_authentication) + child_exec_exim(CEE_EXEC_EXIT, FALSE, NULL, FALSE, 9, + US "-odi", US"-t", US"-oem", US"-oi", US"-f", sender, US"-oMas", + sender_authentication, message_id_option); + else + child_exec_exim(CEE_EXEC_EXIT, FALSE, NULL, FALSE, 7, + US "-odi", US"-t", US"-oem", US"-oi", US"-f", sender, + message_id_option); + /* Control does not return here. */ + } + else /* Not test harness */ + { + if (sender_authentication) + child_exec_exim(CEE_EXEC_EXIT, FALSE, NULL, FALSE, 8, + US"-t", US"-oem", US"-oi", US"-f", sender, US"-oMas", + sender_authentication, message_id_option); + else + child_exec_exim(CEE_EXEC_EXIT, FALSE, NULL, FALSE, 6, + US"-t", US"-oem", US"-oi", US"-f", sender, message_id_option); + /* Control does not return here. */ + } + } + +testharness_pause_ms(100); /* let child work even longer, for exec */ + +/* Parent process. Save fork() errno and close the reading end of the stdin +pipe. */ + +save_errno = errno; +(void)close(pfd[pipe_read]); + +/* Fork succeeded */ + +if (pid > 0) + { + *fdptr = pfd[pipe_write]; /* return writing end of stdin pipe */ + return pid; /* and pid of new process */ + } + +/* Fork failed */ + +(void)close(pfd[pipe_write]); +errno = save_errno; +return (pid_t)(-1); +} +#endif /* STAND_ALONE */ + + + +/************************************************* +* Create a non-Exim child process * +*************************************************/ + +/* This function creates a child process and runs the given command in it. It +sets up pipes to the standard input and output of the new process, and returns +them to the caller. The standard error is cloned to the output. If there are +any file descriptors "in the way" in the new process, they are closed. A new +umask is supplied for the process, and an optional new uid and gid are also +available. These are used by the queryprogram router to set an unprivileged id. +SIGUSR1 is always disabled in the new process, as it is not going to be running +Exim (the function child_open_exim() is provided for that). This function +returns the pid of the new process, or -1 if things go wrong. + +Arguments: + argv the argv for exec in the new process + envp the envp for exec in the new process + newumask umask to set in the new process + newuid point to uid for the new process or NULL for no change + newgid point to gid for the new process or NULL for no change + infdptr pointer to int into which the fd of the stdin of the new process + is placed + outfdptr pointer to int into which the fd of the stdout/stderr of the new + process is placed + wd if not NULL, a path to be handed to chdir() in the new process + make_leader if TRUE, make the new process a process group leader + purpose for debug: reason for running the task + +Returns: the pid of the created process or -1 if anything has gone wrong +*/ + +pid_t +child_open_uid(const uschar **argv, const uschar **envp, int newumask, + uid_t *newuid, gid_t *newgid, int *infdptr, int *outfdptr, uschar *wd, + BOOL make_leader, const uschar * purpose) +{ +int save_errno; +int inpfd[2], outpfd[2]; +pid_t pid; + +if (is_tainted(argv[0])) + { + log_write(0, LOG_MAIN | LOG_PANIC, "Attempt to exec tainted path: '%s'", argv[0]); + errno = EPERM; + return (pid_t)(-1); + } + +/* Create the pipes. */ + +if (pipe(inpfd) != 0) return (pid_t)(-1); +if (pipe(outpfd) != 0) + { + (void)close(inpfd[pipe_read]); + (void)close(inpfd[pipe_write]); + return (pid_t)(-1); + } + +/* Fork the process. Ensure that SIGCHLD is set to SIG_DFL before forking, so +that the child process can be waited for. We sometimes get here with it set +otherwise. Save the old state for resetting on the wait. */ + +oldsignal = signal(SIGCHLD, SIG_DFL); +pid = exim_fork(purpose); + +/* Handle the child process. First, set the required environment. We must do +this before messing with the pipes, in order to be able to write debugging +output when things go wrong. */ + +if (pid == 0) + { + signal(SIGUSR1, SIG_IGN); + signal(SIGPIPE, SIG_DFL); + + if (newgid && setgid(*newgid) < 0) + { + DEBUG(D_any) debug_printf("failed to set gid=%ld in subprocess: %s\n", + (long int)(*newgid), strerror(errno)); + goto CHILD_FAILED; + } + + if (newuid && setuid(*newuid) < 0) + { + DEBUG(D_any) debug_printf("failed to set uid=%ld in subprocess: %s\n", + (long int)(*newuid), strerror(errno)); + goto CHILD_FAILED; + } + + (void)umask(newumask); + + if (wd && Uchdir(wd) < 0) + { + DEBUG(D_any) debug_printf("failed to chdir to %s: %s\n", wd, + strerror(errno)); + goto CHILD_FAILED; + } + + /* Becomes a process group leader if requested, and then organize the pipes. + Any unexpected failure is signalled with EX_EXECFAILED; these are all "should + never occur" failures, except for exec failing because the command doesn't + exist. */ + + if (make_leader && setpgid(0,0) < 0) + { + DEBUG(D_any) debug_printf("failed to set group leader in subprocess: %s\n", + strerror(errno)); + goto CHILD_FAILED; + } + + (void)close(inpfd[pipe_write]); + force_fd(inpfd[pipe_read], 0); + + (void)close(outpfd[pipe_read]); + force_fd(outpfd[pipe_write], 1); + + (void)close(2); + (void)dup2(1, 2); + + /* Now do the exec */ + + if (envp) execve(CS argv[0], (char *const *)argv, (char *const *)envp); + else execv(CS argv[0], (char *const *)argv); + + /* Failed to execv. Signal this failure using EX_EXECFAILED. We are + losing the actual errno we got back, because there is no way to return + this information. */ + + CHILD_FAILED: + _exit(EX_EXECFAILED); /* Note: must be _exit(), NOT exit() */ + } + +/* Parent process. Save any fork failure code, and close the reading end of the +stdin pipe, and the writing end of the stdout pipe. */ + +save_errno = errno; +(void)close(inpfd[pipe_read]); +(void)close(outpfd[pipe_write]); + +/* Fork succeeded; return the input/output pipes and the pid */ + +if (pid > 0) + { + *infdptr = inpfd[pipe_write]; + *outfdptr = outpfd[pipe_read]; + return pid; + } + +/* Fork failed; reset fork errno before returning */ + +(void)close(inpfd[pipe_write]); +(void)close(outpfd[pipe_read]); +errno = save_errno; +return (pid_t)(-1); +} + + + + +/************************************************* +* Create child process without uid change * +*************************************************/ + +/* This function is a wrapper for child_open_uid() that doesn't have the uid, +gid and working directory changing arguments. The function is provided so as to +have a clean interface for use from local_scan(), but also saves writing NULL +arguments several calls that would otherwise use child_open_uid(). + +Arguments: + argv the argv for exec in the new process + envp the envp for exec in the new process + newumask umask to set in the new process + infdptr pointer to int into which the fd of the stdin of the new process + is placed + outfdptr pointer to int into which the fd of the stdout/stderr of the new + process is placed + make_leader if TRUE, make the new process a process group leader + purpose for debug: reason for running the task + +Returns: the pid of the created process or -1 if anything has gone wrong +*/ + +pid_t +child_open_function(uschar **argv, uschar **envp, int newumask, int *infdptr, + int *outfdptr, BOOL make_leader, const uschar * purpose) +{ +return child_open_uid(CUSS argv, CUSS envp, newumask, NULL, NULL, + infdptr, outfdptr, NULL, make_leader, purpose); +} + + + + +/************************************************* +* Close down child process * +*************************************************/ + +/* Wait for the given process to finish, with optional timeout. + +Arguments + pid: the pid to wait for + timeout: maximum time to wait; 0 means for as long as it takes + +Returns: >= 0 process terminated by exiting; value is process + ending status; if an execve() failed, the value + is typically 127 (defined as EX_EXECFAILED) + < 0 & > -256 process was terminated by a signal; value is the + negation of the signal number + -256 timed out + -257 other error in wait(); errno still set +*/ + +int +child_close(pid_t pid, int timeout) +{ +int yield; + +if (timeout > 0) + { + sigalrm_seen = FALSE; + ALARM(timeout); + } + +for(;;) + { + int status; + pid_t rc = waitpid(pid, &status, 0); + if (rc == pid) + { + int lowbyte = status & 255; + yield = lowbyte == 0 ? (status >> 8) & 255 : -lowbyte; + break; + } + if (rc < 0) + { + /* This "shouldn't happen" test does happen on MacOS: for some reason + I do not understand we seems to get an alarm signal despite not having + an active alarm set. There seems to be only one, so just go round again. */ + + if (errno == EINTR && sigalrm_seen && timeout <= 0) continue; + + yield = (errno == EINTR && sigalrm_seen) ? -256 : -257; + break; + } + } + +if (timeout > 0) ALARM_CLR(0); + +signal(SIGCHLD, oldsignal); /* restore */ +return yield; +} + +/* End of child.c */ diff --git a/src/cnumber.h b/src/cnumber.h new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/src/cnumber.h @@ -0,0 +1 @@ +1 diff --git a/src/config.h.defaults b/src/config.h.defaults new file mode 100644 index 0000000..25ab755 --- /dev/null +++ b/src/config.h.defaults @@ -0,0 +1,235 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2018 - 2022 */ +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* The default settings for Exim configuration variables. A #define without +any data just defines the existence of the variable; it won't get included +in config.h unless some value is defined in Local/Makefile. If there is data, +it's a default value. + +Do not put spaces between # and the 'define'. +*/ + +#define ALT_CONFIG_PREFIX +#define TRUSTED_CONFIG_LIST + +#define APPENDFILE_MODE 0600 +#define APPENDFILE_DIRECTORY_MODE 0700 +#define APPENDFILE_LOCKFILE_MODE 0600 + +#define AUTH_CRAM_MD5 +#define AUTH_CYRUS_SASL +#define AUTH_DOVECOT +#define AUTH_EXTERNAL +#define AUTH_GSASL +#define AUTH_HEIMDAL_GSSAPI +#define AUTH_PLAINTEXT +#define AUTH_SPA +#define AUTH_TLS + +#define AUTH_VARS 4 + +#define BIN_DIRECTORY + +#define CONFIGURE_FILE +#define CONFIGURE_FILE_USE_EUID +#define CONFIGURE_FILE_USE_NODE +#define CONFIGURE_GROUP +#define CONFIGURE_OWNER +#define CYRUS_PWCHECK_SOCKET +#define CYRUS_SASLAUTHD_SOCKET + +#define DEFAULT_CRYPT crypt +#define DELIVER_IN_BUFFER_SIZE 8192 +#define DELIVER_OUT_BUFFER_SIZE 8192 + +#define DISABLE_CLIENT_CMD_LOG +#define DISABLE_D_OPTION +#define DISABLE_DNSSEC +#define DISABLE_DKIM +#define DISABLE_EVENT +#define DISABLE_OCSP +#define DISABLE_PIPE_CONNECT +#define DISABLE_PRDR +#define DISABLE_QUEUE_RAMP +#define DISABLE_TLS +#define DISABLE_TLS_RESUME + +#define ENABLE_DISABLE_FSYNC + +#define EXIMDB_DIRECTORY_MODE 0750 +#define EXIMDB_LOCK_TIMEOUT 60 +#define EXIMDB_LOCKFILE_MODE 0640 +#define EXIMDB_MODE 0640 +#define EXIM_CLIENT_DH_MIN_MIN_BITS 512 +#define EXIM_CLIENT_DH_DEFAULT_MIN_BITS 1024 +#define EXIM_GNUTLS_LIBRARY_LOG_LEVEL +#define EXIM_SERVER_DH_BITS_PRE2_12 +#define EXIM_PERL +/* Both uid and gid are triggered by this */ +#define EXIM_UID +#define EXPAND_DLFUNC +#define EXPAND_LISTMATCH_RHS + +#define FIXED_NEVER_USERS "root" + +#define HAVE_CRYPT16 +#define HAVE_LOCAL_SCAN +#define HAVE_SA_LEN +#define HEADERS_CHARSET "ISO-8859-1" +#define HEADER_ADD_BUFFER_SIZE (8192 * 4) +#define HEADER_MAXSIZE (1024*1024) + +#define INPUT_DIRECTORY_MODE 0750 +#define IPV6_USE_INET_PTON + +#define LDAP_LIB_TYPE +#define LOCAL_SCAN_HAS_OPTIONS + +#define LOG_DIRECTORY_MODE 0750 +#define LOG_FILE_PATH +#define LOG_MODE 0640 + +#define LOOKUP_CDB +#define LOOKUP_DBM +#define LOOKUP_DNSDB +#define LOOKUP_DSEARCH +#define LOOKUP_IBASE +#define LOOKUP_JSON +#define LOOKUP_LDAP +#define LOOKUP_LMDB +#define LOOKUP_LSEARCH +#define LOOKUP_MYSQL +#define LOOKUP_NIS +#define LOOKUP_NISPLUS +#define LOOKUP_ORACLE +#define LOOKUP_PASSWD +#define LOOKUP_PGSQL +#define LOOKUP_REDIS +#define LOOKUP_SQLITE +#define LOOKUP_TESTDB +#define LOOKUP_WHOSON +#define LOOKUP_WILDLSEARCH +#define LOOKUP_NWILDLSEARCH + +#define LOOKUP_MODULE_DIR + +#define MAX_FILTER_SIZE (1024*1024) +#define MAX_LOCALHOST_NUMBER 256 +#define MAX_INCLUDE_SIZE (1024*1024) +#define MAX_INTERFACES 250 +#define MAX_NAMED_LIST 16 +#define MSGLOG_DIRECTORY_MODE 0750 + +#define NVALGRIND + +#define PID_FILE_PATH + +#define RADIUS_CONFIG_FILE +#define RADIUS_LIB_TYPE + +#define REGEX_VARS 9 + +#define ROUTER_ACCEPT +#define ROUTER_DNSLOOKUP +#define ROUTER_IPLITERAL +#define ROUTER_IPLOOKUP +#define ROUTER_MANUALROUTE +#define ROUTER_QUERYPROGRAM +#define ROUTER_REDIRECT + +#define SPOOL_DIRECTORY +#define SPOOL_DIRECTORY_MODE 0750 +#define SPOOL_MODE 0640 +#define STRING_SPRINTF_BUFFER_SIZE (8192 * 4) + +#define SUPPORT_CRYPTEQ +#define SUPPORT_DANE +#define SUPPORT_DMARC +#define DMARC_TLD_FILE "/etc/exim/opendmarc.tlds" +#define SUPPORT_I18N +#define SUPPORT_I18N_2008 +#define SUPPORT_MAILDIR +#define SUPPORT_MAILSTORE +#define SUPPORT_MBX +#define SUPPORT_MOVE_FROZEN_MESSAGES +#define SUPPORT_PAM +#define SUPPORT_PROXY +#define SUPPORT_SOCKS +#define SUPPORT_SPF +#define SUPPORT_SRS +#define SUPPORT_TRANSLATE_IP_ADDRESS + +#define SYSLOG_LOG_PID +#define SYSLOG_LONG_LINES + +#define TCP_WRAPPERS_DAEMON_NAME "exim" +#define TIMEZONE_DEFAULT +#define EXIM_TMPDIR + +#define TRANSPORT_APPENDFILE +#define TRANSPORT_AUTOREPLY +#define TRANSPORT_LMTP +#define TRANSPORT_PIPE +#define TRANSPORT_SMTP + +#define USE_DB +#define USE_GDBM +#define USE_GNUTLS +#define AVOID_GNUTLS_PKCS11 +#define USE_NDBM +#define USE_OPENSSL +#define USE_READLINE +#define USE_TCP_WRAPPERS +#define USE_TDB + +#define WHITELIST_D_MACROS + +#define WITH_CONTENT_SCAN +#define DISABLE_MAL_FFROTD +#define DISABLE_MAL_FFROT6D +#define DISABLE_MAL_DRWEB +#define DISABLE_MAL_AVE +#define DISABLE_MAL_FSECURE +#define DISABLE_MAL_KAV +#define DISABLE_MAL_SOPHIE +#define DISABLE_MAL_CLAM +#define DISABLE_MAL_MKS +#define DISABLE_MAL_AVAST +#define DISABLE_MAL_SOCK +#define DISABLE_MAL_CMDLINE + +/* EXPERIMENTAL features */ +#define EXPERIMENTAL_ARC +#define EXPERIMENTAL_BRIGHTMAIL +#define EXPERIMENTAL_DCC +#define EXPERIMENTAL_DSN_INFO +#define EXPERIMENTAL_ESMTP_LIMITS +#define EXPERIMENTAL_QUEUEFILE + + +/* For developers */ +#define WANT_DEEPER_PRINTF_CHECKS + +/* Things that are not routinely changed but are nevertheless configurable +just in case. */ + +#define DNS_MAXNAME 1024 +#define EXPAND_MAXN 20 +#define ROOT_UID 0 +#define ROOT_GID 0 + +/* Sizes for integer arithmetic. +Go for 64bit; can be overridden in OS/Makefile-FOO +If you make it a different number of bits, provide a definition +for EXIM_ARITH_MAX and _MIN in OS/oh.h-FOO */ +#define int_eximarith_t int64_t +#define PR_EXIM_ARITH "%" PRId64 /* C99 standard, printf %lld */ +#define SC_EXIM_ARITH "%" SCNi64 /* scanf incl. 0x prefix */ +#define SC_EXIM_DEC "%" SCNd64 /* scanf decimal */ + +/* End of config.h.defaults */ diff --git a/src/configure.default b/src/configure.default new file mode 100644 index 0000000..3761daf --- /dev/null +++ b/src/configure.default @@ -0,0 +1,1015 @@ +###################################################################### +# Runtime configuration file for Exim # +###################################################################### + + +# This is a default configuration file which will operate correctly in +# uncomplicated installations. Please see the manual for a complete list +# of all the runtime configuration options that can be included in a +# configuration file. There are many more than are mentioned here. The +# manual is in the file doc/spec.txt in the Exim distribution as a plain +# ASCII file. Other formats (PostScript, Texinfo, HTML, PDF) are available +# from the Exim ftp sites. The manual is also online at the Exim website. + + +# This file is divided into several parts, all but the first of which are +# headed by a line starting with the word "begin". Only those parts that +# are required need to be present. Blank lines, and lines starting with # +# are ignored. + + +########### IMPORTANT ########## IMPORTANT ########### IMPORTANT ########### +# # +# Whenever you change Exim's configuration file, you *must* remember to # +# HUP the Exim daemon, because it will not pick up the new configuration # +# until you do. However, any other Exim processes that are started, for # +# example, a process started by an MUA in order to send a message, will # +# see the new configuration as soon as it is in place. # +# # +# You do not need to HUP the daemon for changes in auxiliary files that # +# are referenced from this file. They are read every time they are used. # +# # +# It is usually a good idea to test a new configuration for syntactic # +# correctness before installing it (for example, by running the command # +# "exim -C /config/file.new -bV"). # +# # +########### IMPORTANT ########## IMPORTANT ########### IMPORTANT ########### + + + +###################################################################### +# MACROS # +###################################################################### +# + +# If you want to use a smarthost instead of sending directly to recipient +# domains, uncomment this macro definition and set a real hostname. +# An appropriately privileged user can then redirect email on the command-line +# in emergencies, via -D. +# +# ROUTER_SMARTHOST=MAIL.HOSTNAME.FOR.CENTRAL.SERVER.EXAMPLE + +###################################################################### +# MAIN CONFIGURATION SETTINGS # +###################################################################### +# + +# Specify your host's canonical name here. This should normally be the fully +# qualified "official" name of your host. If this option is not set, the +# uname() function is called to obtain the name. In many cases this does +# the right thing and you need not set anything explicitly. + +# primary_hostname = + + +# The next three settings create two lists of domains and one list of hosts. +# These lists are referred to later in this configuration using the syntax +# +local_domains, +relay_to_domains, and +relay_from_hosts, respectively. They +# are all colon-separated lists: + +domainlist local_domains = @ +domainlist relay_to_domains = +hostlist relay_from_hosts = localhost +# (We rely upon hostname resolution working for localhost, because the default +# uncommented configuration needs to work in IPv4-only environments.) + +# Most straightforward access control requirements can be obtained by +# appropriate settings of the above options. In more complicated situations, +# you may need to modify the Access Control Lists (ACLs) which appear later in +# this file. + +# The first setting specifies your local domains, for example: +# +# domainlist local_domains = my.first.domain : my.second.domain +# +# You can use "@" to mean "the name of the local host", as in the default +# setting above. This is the name that is specified by primary_hostname, +# as specified above (or defaulted). If you do not want to do any local +# deliveries, remove the "@" from the setting above. If you want to accept mail +# addressed to your host's literal IP address, for example, mail addressed to +# "user@[192.168.23.44]", you can add "@[]" as an item in the local domains +# list. You also need to uncomment "allow_domain_literals" below. This is not +# recommended for today's Internet. + +# The second setting specifies domains for which your host is an incoming relay. +# If you are not doing any relaying, you should leave the list empty. However, +# if your host is an MX backup or gateway of some kind for some domains, you +# must set relay_to_domains to match those domains. For example: +# +# domainlist relay_to_domains = *.myco.com : my.friend.org +# +# This will allow any host to relay through your host to those domains. +# See the section of the manual entitled "Control of relaying" for more +# information. + +# The third setting specifies hosts that can use your host as an outgoing relay +# to any other host on the Internet. Such a setting commonly refers to a +# complete local network as well as the localhost. For example: +# +# hostlist relay_from_hosts = <; 127.0.0.1 ; ::1 ; 192.168.0.0/16 +# +# The "/16" is a bit mask (CIDR notation), not a number of hosts. Note that you +# have to include 127.0.0.1 if you want to allow processes on your host to send +# SMTP mail by using the loopback address. A number of MUAs use this method of +# sending mail. Often, connections are made to "localhost", which might be ::1 +# on IPv6-enabled hosts. Do not forget CIDR for your IPv6 networks. + +# All three of these lists may contain many different kinds of item, including +# wildcarded names, regular expressions, and file lookups. See the reference +# manual for details. The lists above are used in the access control lists for +# checking incoming messages. The names of these ACLs are defined here: + +acl_smtp_rcpt = acl_check_rcpt +.ifdef _HAVE_PRDR +acl_smtp_data_prdr = acl_check_prdr +.endif +acl_smtp_data = acl_check_data + +# You should not change those settings until you understand how ACLs work. + + +# If you are running a version of Exim that was compiled with the content- +# scanning extension, you can cause incoming messages to be automatically +# scanned for viruses. You have to modify the configuration in two places to +# set this up. The first of them is here, where you define the interface to +# your scanner. This example is typical for ClamAV; see the manual for details +# of what to set for other virus scanners. The second modification is in the +# acl_check_data access control list (see below). + +# av_scanner = clamd:/tmp/clamd + + +# For spam scanning, there is a similar option that defines the interface to +# SpamAssassin. You do not need to set this if you are using the default, which +# is shown in this commented example. As for virus scanning, you must also +# modify the acl_check_data access control list to enable spam scanning. + +# spamd_address = 127.0.0.1 783 + + +# If Exim is compiled with support for TLS, you may want to change the +# following option so that Exim disallows certain clients from makeing encrypted +# connections. The default is to allow all. +# In the authenticators section below, there are template configurations for +# plaintext username/password authentication. This kind of authentication is +# only safe when used within a TLS connection, so the authenticators will only +# work if TLS is allowed here. + +# This is equivalent to the default. + +# tls_advertise_hosts = * + +# Specify the location of the Exim server's TLS certificate and private key. +# The private key must not be encrypted (password protected). You can put +# the certificate and private key in the same file, in which case you only +# need the first setting, or in separate files, in which case you need both +# options. + +# tls_certificate = /etc/ssl/exim.crt +# tls_privatekey = /etc/ssl/exim.pem + +# For OpenSSL, prefer EC- over RSA-authenticated ciphers +.ifdef _HAVE_OPENSSL +tls_require_ciphers = ECDSA:RSA:!COMPLEMENTOFDEFAULT +.endif + +# Don't offer resumption to (most) MUAs, who we don't want to reuse +# tickets. Once the TLS extension for vended ticket numbers comes +# though, re-examine since resumption on a single-use ticket is still a benefit. +.ifdef _HAVE_TLS_RESUME +tls_resumption_hosts = ${if inlist {$received_port}{587:465} {:}{*}} +.endif + +# In order to support roaming users who wish to send email from anywhere, +# you may want to make Exim listen on other ports as well as port 25, in +# case these users need to send email from a network that blocks port 25. +# The standard port for this purpose is port 587, the "message submission" +# port. See RFC 4409 for details. Microsoft MUAs cannot be configured to +# talk the message submission protocol correctly, so if you need to support +# them you should also allow TLS-on-connect on the traditional but +# non-standard port 465. + +# daemon_smtp_ports = 25 : 465 : 587 +# tls_on_connect_ports = 465 + + +# Specify the domain you want to be added to all unqualified addresses +# here. An unqualified address is one that does not contain an "@" character +# followed by a domain. For example, "caesar@rome.example" is a fully qualified +# address, but the string "caesar" (i.e. just a login name) is an unqualified +# email address. Unqualified addresses are accepted only from local callers by +# default. See the recipient_unqualified_hosts option if you want to permit +# unqualified addresses from remote sources. If this option is not set, the +# primary_hostname value is used for qualification. + +# qualify_domain = + + +# If you want unqualified recipient addresses to be qualified with a different +# domain to unqualified sender addresses, specify the recipient domain here. +# If this option is not set, the qualify_domain value is used. + +# qualify_recipient = + + +# The following line must be uncommented if you want Exim to recognize +# addresses of the form "user@[10.11.12.13]" that is, with a "domain literal" +# (an IP address) instead of a named domain. The RFCs still require this form, +# but it makes little sense to permit mail to be sent to specific hosts by +# their IP address in the modern Internet. This ancient format has been used +# by those seeking to abuse hosts by using them for unwanted relaying. If you +# really do want to support domain literals, uncomment the following line, and +# see also the "domain_literal" router below. + +# allow_domain_literals + + +# No deliveries will ever be run under the uids of users specified by +# never_users (a colon-separated list). An attempt to do so causes a panic +# error to be logged, and the delivery to be deferred. This is a paranoic +# safety catch. There is an even stronger safety catch in the form of the +# FIXED_NEVER_USERS setting in the configuration for building Exim. The list of +# users that it specifies is built into the binary, and cannot be changed. The +# option below just adds additional users to the list. The default for +# FIXED_NEVER_USERS is "root", but just to be absolutely sure, the default here +# is also "root". + +# Note that the default setting means you cannot deliver mail addressed to root +# as if it were a normal user. This isn't usually a problem, as most sites have +# an alias for root that redirects such mail to a human administrator. + +never_users = root + + +# The setting below causes Exim to do a reverse DNS lookup on all incoming +# IP calls, in order to get the true host name. If you feel this is too +# expensive, you can specify the networks for which a lookup is done, or +# remove the setting entirely. + +host_lookup = * + + +# The setting below causes Exim to try to initialize the system resolver +# library with DNSSEC support. It has no effect if your library lacks +# DNSSEC support. + +dns_dnssec_ok = 1 + + +# The settings below cause Exim to make RFC 1413 (ident) callbacks +# for all incoming SMTP calls. You can limit the hosts to which these +# calls are made, and/or change the timeout that is used. If you set +# the timeout to zero, all RFC 1413 calls are disabled. RFC 1413 calls +# are cheap and can provide useful information for tracing problem +# messages, but some hosts and firewalls have problems with them. +# This can result in a timeout instead of an immediate refused +# connection, leading to delays on starting up SMTP sessions. +# (The default was reduced from 30s to 5s for release 4.61. and to +# disabled for release 4.86) +# +#rfc1413_hosts = * +#rfc1413_query_timeout = 5s + + +# Enable an efficiency feature. We advertise the feature; clients +# may request to use it. For multi-recipient mails we then can +# reject or accept per-user after the message is received. +# This supports recipient-dependent content filtering; without it +# you have to temp-reject any recipients after the first that have +# incompatible filtering, and do the filtering in the data ACL. +# Even with this enabled, you must support the old style for peers +# not flagging support for PRDR (visible via $prdr_requested). +# +.ifdef _HAVE_PRDR +prdr_enable = true +.endif + + +# By default, Exim expects all envelope addresses to be fully qualified, that +# is, they must contain both a local part and a domain. If you want to accept +# unqualified addresses (just a local part) from certain hosts, you can specify +# these hosts by setting one or both of +# +# sender_unqualified_hosts = +# recipient_unqualified_hosts = +# +# to control sender and recipient addresses, respectively. When this is done, +# unqualified addresses are qualified using the settings of qualify_domain +# and/or qualify_recipient (see above). + + +# Unless you run a high-volume site you probably want more logging +# detail than the default. Adjust to suit. + +log_selector = +smtp_protocol_error +smtp_syntax_error \ + +tls_certificate_verified + + +# If you want Exim to support the "percent hack" for certain domains, +# uncomment the following line and provide a list of domains. The "percent +# hack" is the feature by which mail addressed to x%y@z (where z is one of +# the domains listed) is locally rerouted to x@y and sent on. If z is not one +# of the "percent hack" domains, x%y is treated as an ordinary local part. This +# hack is rarely needed nowadays; you should not enable it unless you are sure +# that you really need it. +# +# percent_hack_domains = +# +# As well as setting this option you will also need to remove the test +# for local parts containing % in the ACL definition below. + + +# When Exim can neither deliver a message nor return it to sender, it "freezes" +# the delivery error message (aka "bounce message"). There are also other +# circumstances in which messages get frozen. They will stay on the queue for +# ever unless one of the following options is set. + +# This option unfreezes frozen bounce messages after two days, tries +# once more to deliver them, and ignores any delivery failures. + +ignore_bounce_errors_after = 2d + +# This option cancels (removes) frozen messages that are older than a week. + +timeout_frozen_after = 7d + + +# By default, messages that are waiting on Exim's queue are all held in a +# single directory called "input" which is itself within Exim's spool +# directory. (The default spool directory is specified when Exim is built, and +# is often /var/spool/exim/.) Exim works best when its queue is kept short, but +# there are circumstances where this is not always possible. If you uncomment +# the setting below, messages on the queue are held in 62 subdirectories of +# "input" instead of all in the same directory. The subdirectories are called +# 0, 1, ... A, B, ... a, b, ... z. This has two benefits: (1) If your file +# system degrades with many files in one directory, this is less likely to +# happen; (2) Exim can process the queue one subdirectory at a time instead of +# all at once, which can give better performance with large queues. + +# split_spool_directory = true + + +# If you're in a part of the world where ASCII is not sufficient for most +# text, then you're probably familiar with RFC2047 message header extensions. +# By default, Exim adheres to the specification, including a limit of 76 +# characters to a line, with encoded words fitting within a line. +# If you wish to use decoded headers in message filters in such a way +# that successful decoding of malformed messages matters, you may wish to +# configure Exim to be more lenient. +# +# check_rfc2047_length = false +# +# In particular, the Exim maintainers have had multiple reports of problems +# from Russian administrators of issues until they disable this check, +# because of some popular, yet buggy, mail composition software. + + +# If you wish to be strictly RFC compliant, or if you know you'll be +# exchanging email with systems that are not 8-bit clean, then you may +# wish to disable advertising 8BITMIME. Uncomment this option to do so. + +# accept_8bitmime = false + + +# Exim does not make use of environment variables itself. However, +# libraries that Exim uses (e.g. LDAP) depend on specific environment settings. +# There are two lists: keep_environment for the variables we trust, and +# add_environment for variables we want to set to a specific value. +# Note that TZ is handled separately by the timezone runtime option +# and TIMEZONE_DEFAULT buildtime option. + +# keep_environment = ^LDAP +# add_environment = PATH=/usr/bin::/bin + + + +###################################################################### +# ACL CONFIGURATION # +# Specifies access control lists for incoming SMTP mail # +###################################################################### + +begin acl + +# This access control list is used for every RCPT command in an incoming +# SMTP message. The tests are run in order until the address is either +# accepted or denied. + +acl_check_rcpt: + + # Accept if the source is local SMTP (i.e. not over TCP/IP). We do this by + # testing for an empty sending host field. + + accept hosts = : + control = dkim_disable_verify + + ############################################################################# + # The following section of the ACL is concerned with local parts that contain + # @ or % or ! or / or | or dots in unusual places. + # + # The characters other than dots are rarely found in genuine local parts, but + # are often tried by people looking to circumvent relaying restrictions. + # Therefore, although they are valid in local parts, these rules lock them + # out, as a precaution. + # + # Empty components (two dots in a row) are not valid in RFC 2822, but Exim + # allows them because they have been encountered. (Consider local parts + # constructed as "firstinitial.secondinitial.familyname" when applied to + # someone like me, who has no second initial.) However, a local part starting + # with a dot or containing /../ can cause trouble if it is used as part of a + # file name (e.g. for a mailing list). This is also true for local parts that + # contain slashes. A pipe symbol can also be troublesome if the local part is + # incorporated unthinkingly into a shell command line. + # + # Two different rules are used. The first one is stricter, and is applied to + # messages that are addressed to one of the local domains handled by this + # host. The line "domains = +local_domains" restricts it to domains that are + # defined by the "domainlist local_domains" setting above. The rule blocks + # local parts that begin with a dot or contain @ % ! / or |. If you have + # local accounts that include these characters, you will have to modify this + # rule. + + deny message = Restricted characters in address + domains = +local_domains + local_parts = ^[.] : ^.*[@%!/|] + + # The second rule applies to all other domains, and is less strict. The line + # "domains = !+local_domains" restricts it to domains that are NOT defined by + # the "domainlist local_domains" setting above. The exclamation mark is a + # negating operator. This rule allows your own users to send outgoing + # messages to sites that use slashes and vertical bars in their local parts. + # It blocks local parts that begin with a dot, slash, or vertical bar, but + # allows these characters within the local part. However, the sequence /../ + # is barred. The use of @ % and ! is blocked, as before. The motivation here + # is to prevent your users (or your users' viruses) from mounting certain + # kinds of attack on remote sites. + + deny message = Restricted characters in address + domains = !+local_domains + local_parts = ^[./|] : ^.*[@%!] : ^.*/\\.\\./ + ############################################################################# + + # Accept mail to postmaster in any local domain, regardless of the source, + # and without verifying the sender. + + accept local_parts = postmaster + domains = +local_domains + + # Deny unless the sender address can be verified. + + require verify = sender + + # Reject all RCPT commands after too many bad recipients + # This is partly a defense against spam abuse and partly attacker abuse. + # Real senders should manage, by the time they get to 10 RCPT directives, + # to have had at least half of them be real addresses. + # + # This is a lightweight check and can protect you against repeated + # invocations of more heavy-weight checks which would come after it. + + deny condition = ${if and {\ + {>{$rcpt_count}{10}}\ + {<{$recipients_count}{${eval:$rcpt_count/2}}} }} + message = Rejected for too many bad recipients + logwrite = REJECT [$sender_host_address]: bad recipient count high [${eval:$rcpt_count-$recipients_count}] + + # Accept if the message comes from one of the hosts for which we are an + # outgoing relay. It is assumed that such hosts are most likely to be MUAs, + # so we set control=submission to make Exim treat the message as a + # submission. It will fix up various errors in the message, for example, the + # lack of a Date: header line. If you are actually relaying out out from + # MTAs, you may want to disable this. If you are handling both relaying from + # MTAs and submissions from MUAs you should probably split them into two + # lists, and handle them differently. + + # Recipient verification is omitted here, because in many cases the clients + # are dumb MUAs that don't cope well with SMTP error responses. If you are + # actually relaying out from MTAs, you should probably add recipient + # verification here. + + # Note that, by putting this test before any DNS black list checks, you will + # always accept from these hosts, even if they end up on a black list. The + # assumption is that they are your friends, and if they get onto a black + # list, it is a mistake. + + accept hosts = +relay_from_hosts + control = submission + control = dkim_disable_verify + + # Accept if the message arrived over an authenticated connection, from + # any host. Again, these messages are usually from MUAs, so recipient + # verification is omitted, and submission mode is set. And again, we do this + # check before any black list tests. + + accept authenticated = * + control = submission + control = dkim_disable_verify + + # Insist that any other recipient address that we accept is either in one of + # our local domains, or is in a domain for which we explicitly allow + # relaying. Any other domain is rejected as being unacceptable for relaying. + + require message = relay not permitted + domains = +local_domains : +relay_to_domains + + # We also require all accepted addresses to be verifiable. This check will + # do local part verification for local domains, but only check the domain + # for remote domains. The only way to check local parts for the remote + # relay domains is to use a callout (add /callout), but please read the + # documentation about callouts before doing this. + + require verify = recipient + + ############################################################################# + # There are no default checks on DNS black lists because the domains that + # contain these lists are changing all the time. However, here are two + # examples of how you can get Exim to perform a DNS black list lookup at this + # point. The first one denies, whereas the second just warns. + # + # deny dnslists = black.list.example + # message = rejected because $sender_host_address is in a black list at $dnslist_domain\n$dnslist_text + # + # warn dnslists = black.list.example + # add_header = X-Warning: $sender_host_address is in a black list at $dnslist_domain + # log_message = found in $dnslist_domain + ############################################################################# + + ############################################################################# + # This check is commented out because it is recognized that not every + # sysadmin will want to do it. If you enable it, the check performs + # Client SMTP Authorization (csa) checks on the sending host. These checks + # do DNS lookups for SRV records. The CSA proposal is currently (May 2005) + # an Internet draft. You can, of course, add additional conditions to this + # ACL statement to restrict the CSA checks to certain hosts only. + # + # require verify = csa + ############################################################################# + + ############################################################################# + # If doing per-user content filtering then recipients with filters different + # to the first recipient must be deferred unless the sender talks PRDR. + # + # defer !condition = $prdr_requested + # condition = ${if > {0}{$recipients_count}} + # condition = ${if !eq {$acl_m_content_filter} \ + # {${lookup PER_RCPT_CONTENT_FILTER}}} + # warn !condition = $prdr_requested + # condition = ${if > {0}{$recipients_count}} + # set acl_m_content_filter = ${lookup PER_RCPT_CONTENT_FILTER} + ############################################################################# + + # At this point, the address has passed all the checks that have been + # configured, so we accept it unconditionally. + + accept + + +# This ACL is used once per recipient, for multi-recipient messages, if +# we advertised PRDR. It can be used to perform receipient-dependent +# header- and body- based filtering and rejections. +# We set a variable to record that PRDR was active used, so that checking +# in the data ACL can be skipped. + +.ifdef _HAVE_PRDR +acl_check_prdr: + warn set acl_m_did_prdr = y + + ############################################################################# + # do lookup on filtering, with $local_part@$domain, deny on filter match + # + # deny set acl_m_content_filter = ${lookup PER_RCPT_CONTENT_FILTER} + # condition = ... + ############################################################################# + + accept +.endif + +# This ACL is used after the contents of a message have been received. This +# is the ACL in which you can test a message's headers or body, and in +# particular, this is where you can invoke external virus or spam scanners. +# Some suggested ways of configuring these tests are shown below, commented +# out. Without any tests, this ACL accepts all messages. If you want to use +# such tests, you must ensure that Exim is compiled with the content-scanning +# extension (WITH_CONTENT_SCAN=yes in Local/Makefile). + +acl_check_data: + + # Deny if the message contains an overlong line. Per the standards + # we should never receive one such via SMTP. + # + deny condition = ${if > {$max_received_linelength}{998}} + message = maximum allowed line length is 998 octets, \ + got $max_received_linelength + + # Deny if the headers contain badly-formed addresses. + # + deny !verify = header_syntax + message = header syntax + log_message = header syntax ($acl_verify_message) + + # Deny if the message contains a virus. Before enabling this check, you + # must install a virus scanner and set the av_scanner option above. + # + # deny malware = * + # message = This message contains a virus ($malware_name). + + # Add headers to a message if it is judged to be spam. Before enabling this, + # you must install SpamAssassin. You may also need to set the spamd_address + # option above. + # + # warn spam = nobody + # add_header = X-Spam_score: $spam_score\n\ + # X-Spam_score_int: $spam_score_int\n\ + # X-Spam_bar: $spam_bar\n\ + # X-Spam_report: $spam_report + + ############################################################################# + # No more tests if PRDR was actively used. + # accept condition = ${if def:acl_m_did_prdr} + # + # To get here, all message recipients must have identical per-user + # content filtering (enforced by RCPT ACL). Do lookup for filter + # and deny on match. + # + # deny set acl_m_content_filter = ${lookup PER_RCPT_CONTENT_FILTER} + # condition = ... + ############################################################################# + + + # Accept the message. + + accept + + + +###################################################################### +# ROUTERS CONFIGURATION # +# Specifies how addresses are handled # +###################################################################### +# THE ORDER IN WHICH THE ROUTERS ARE DEFINED IS IMPORTANT! # +# An address is passed to each router in turn until it is accepted. # +###################################################################### + +begin routers + +# This router routes to remote hosts over SMTP by explicit IP address, +# when an email address is given in "domain literal" form, for example, +# . The RFCs require this facility. However, it is +# little-known these days, and has been exploited by evil people seeking +# to abuse SMTP relays. Consequently it is commented out in the default +# configuration. If you uncomment this router, you also need to uncomment +# allow_domain_literals above, so that Exim can recognize the syntax of +# domain literal addresses. + +# domain_literal: +# driver = ipliteral +# domains = ! +local_domains +# transport = remote_smtp + + +# This router can be used when you want to send all mail to a +# server which handles DNS lookups for you; an ISP will typically run such +# a server for their customers. The hostname in route_data comes from the +# macro defined at the top of the file. If not defined, then we'll use the +# dnslookup router below instead. +# Beware that the hostname is specified again in the Transport. + +.ifdef ROUTER_SMARTHOST + +smarthost: + driver = manualroute + domains = ! +local_domains + transport = smarthost_smtp + route_data = ROUTER_SMARTHOST + ignore_target_hosts = <; 0.0.0.0 ; 127.0.0.0/8 ; ::1 + no_more + +.else + +# This router routes addresses that are not in local domains by doing a DNS +# lookup on the domain name. The exclamation mark that appears in "domains = ! +# +local_domains" is a negating operator, that is, it can be read as "not". The +# recipient's domain must not be one of those defined by "domainlist +# local_domains" above for this router to be used. +# +# If the router is used, any domain that resolves to 0.0.0.0 or to a loopback +# interface address (127.0.0.0/8) is treated as if it had no DNS entry. Note +# that 0.0.0.0 is the same as 0.0.0.0/32, which is commonly treated as the +# local host inside the network stack. It is not 0.0.0.0/0, the default route. +# If the DNS lookup fails, no further routers are tried because of the no_more +# setting, and consequently the address is unrouteable. + +dnslookup: + driver = dnslookup + domains = ! +local_domains + transport = remote_smtp + ignore_target_hosts = 0.0.0.0 : 127.0.0.0/8 +# if ipv6-enabled then instead use: +# ignore_target_hosts = <; 0.0.0.0 ; 127.0.0.0/8 ; ::1 + no_more + +# This closes the ROUTER_SMARTHOST ifdef around the choice of routing for +# off-site mail. +.endif + + +# The remaining routers handle addresses in the local domain(s), that is those +# domains that are defined by "domainlist local_domains" above. + + +# This router handles aliasing using a linearly searched alias file with the +# name SYSTEM_ALIASES_FILE. When this configuration is installed automatically, +# the name gets inserted into this file from whatever is set in Exim's +# build-time configuration. The default path is the traditional /etc/aliases. +# If you install this configuration by hand, you need to specify the correct +# path in the "data" setting below. +# +##### NB You must ensure that the alias file exists. It used to be the case +##### NB that every Unix had that file, because it was the Sendmail default. +##### NB These days, there are systems that don't have it. Your aliases +##### NB file should at least contain an alias for "postmaster". +# +# If any of your aliases expand to pipes or files, you will need to set +# up a user and a group for these deliveries to run under. You can do +# this by uncommenting the "user" option below (changing the user name +# as appropriate) and adding a "group" option if necessary. Alternatively, you +# can specify "user" on the transports that are used. Note that the transports +# listed below are the same as are used for .forward files; you might want +# to set up different ones for pipe and file deliveries from aliases. + +system_aliases: + driver = redirect + allow_fail + allow_defer + data = ${lookup{$local_part}lsearch{SYSTEM_ALIASES_FILE}} +# user = exim + file_transport = address_file + pipe_transport = address_pipe + + +# This router handles forwarding using traditional .forward files in users' +# home directories. If you want it also to allow mail filtering when a forward +# file starts with the string "# Exim filter" or "# Sieve filter", uncomment +# the "allow_filter" option. + +# The no_verify setting means that this router is skipped when Exim is +# verifying addresses. Similarly, no_expn means that this router is skipped if +# Exim is processing an EXPN command. + +# If you want this router to treat local parts with suffixes introduced by "-" +# or "+" characters as if the suffixes did not exist, uncomment the two local_ +# part_suffix options. Then, for example, xxxx-foo@your.domain will be treated +# in the same way as xxxx@your.domain by this router. Because this router is +# not used for verification, if you choose to uncomment those options, then you +# will *need* to make the same change to the localuser router. (There are +# other approaches, if this is undesirable, but they add complexity). + +# The check_ancestor option means that if the forward file generates an +# address that is an ancestor of the current one, the current one gets +# passed on instead. This covers the case where A is aliased to B and B +# has a .forward file pointing to A. + +# The three transports specified at the end are those that are used when +# forwarding generates a direct delivery to a file, or to a pipe, or sets +# up an auto-reply, respectively. + +userforward: + driver = redirect + check_local_user +# local_part_suffix = +* : -* +# local_part_suffix_optional + file = $home/.forward +# allow_filter + no_verify + no_expn + check_ancestor + file_transport = address_file + pipe_transport = address_pipe + reply_transport = address_reply + + +# This router matches local user mailboxes. If the router fails, the error +# message is "Unknown user". + +# If you want this router to treat local parts with suffixes introduced by "-" +# or "+" characters as if the suffixes did not exist, uncomment the two local_ +# part_suffix options. Then, for example, xxxx-foo@your.domain will be treated +# in the same way as xxxx@your.domain by this router. + +localuser: + driver = accept + check_local_user +# local_part_suffix = +* : -* +# local_part_suffix_optional + transport = local_delivery + cannot_route_message = Unknown user + + + +###################################################################### +# TRANSPORTS CONFIGURATION # +###################################################################### +# ORDER DOES NOT MATTER # +# Only one appropriate transport is called for each delivery. # +###################################################################### + +# A transport is used only when referenced from a router that successfully +# handles an address. + +begin transports + + +# This transport is used for delivering messages over SMTP connections. + +remote_smtp: + driver = smtp +.ifdef _HAVE_TLS_RESUME + tls_resumption_hosts = * +.endif + + +# This transport is used for delivering messages to a smarthost, if the +# smarthost router is enabled. This starts from the same basis as +# "remote_smtp" but then turns on various security options, because +# we assume that if you're told "use smarthost.example.org as the smarthost" +# then there will be TLS available, with a verifiable certificate for that +# hostname, using decent TLS. + +smarthost_smtp: + driver = smtp + multi_domain + # +.ifdef _HAVE_TLS + # Comment out any of these which you have to, then file a Support + # request with your smarthost provider to get things fixed: + hosts_require_tls = * + tls_verify_hosts = * + # As long as tls_verify_hosts is enabled, this this will have no effect, + # but if you have to comment it out then this will at least log whether + # you succeed or not: + tls_try_verify_hosts = * + # + # The SNI name should match the name which we'll expect to verify; + # many mail systems don't use SNI and this doesn't matter, but if it does, + # we need to send a name which the remote site will recognize. + # This _should_ be the name which the smarthost operators specified as + # the hostname for sending your mail to. + tls_sni = ROUTER_SMARTHOST + # +.ifdef _HAVE_OPENSSL + tls_require_ciphers = HIGH:!aNULL:@STRENGTH +.endif +.ifdef _HAVE_GNUTLS + tls_require_ciphers = SECURE192:-VERS-SSL3.0:-VERS-TLS1.0:-VERS-TLS1.1 +.endif +.ifdef _HAVE_TLS_RESUME + tls_resumption_hosts = * +.endif +.endif + + +# This transport is used for local delivery to user mailboxes in traditional +# BSD mailbox format. By default it will be run under the uid and gid of the +# local user, and requires the sticky bit to be set on the /var/mail directory. +# Some systems use the alternative approach of running mail deliveries under a +# particular group instead of using the sticky bit. The commented options below +# show how this can be done. + +local_delivery: + driver = appendfile + file = /var/mail/$local_part_data + delivery_date_add + envelope_to_add + return_path_add +# group = mail +# mode = 0660 + + +# This transport is used for handling pipe deliveries generated by alias or +# .forward files. If the pipe generates any standard output, it is returned +# to the sender of the message as a delivery error. Set return_fail_output +# instead of return_output if you want this to happen only when the pipe fails +# to complete normally. You can set different transports for aliases and +# forwards if you want to - see the references to address_pipe in the routers +# section above. + +address_pipe: + driver = pipe + return_output + + +# This transport is used for handling deliveries directly to files that are +# generated by aliasing or forwarding. + +address_file: + driver = appendfile + delivery_date_add + envelope_to_add + return_path_add + + +# This transport is used for handling autoreplies generated by the filtering +# option of the userforward router. + +address_reply: + driver = autoreply + + + +###################################################################### +# RETRY CONFIGURATION # +###################################################################### + +begin retry + +# This single retry rule applies to all domains and all errors. It specifies +# retries every 15 minutes for 2 hours, then increasing retry intervals, +# starting at 1 hour and increasing each time by a factor of 1.5, up to 16 +# hours, then retries every 6 hours until 4 days have passed since the first +# failed delivery. + +# WARNING: If you do not have any retry rules at all (this section of the +# configuration is non-existent or empty), Exim will not do any retries of +# messages that fail to get delivered at the first attempt. The effect will +# be to treat temporary errors as permanent. Therefore, DO NOT remove this +# retry rule unless you really don't want any retries. + +# Address or Domain Error Retries +# ----------------- ----- ------- + +* * F,2h,15m; G,16h,1h,1.5; F,4d,6h + + + +###################################################################### +# REWRITE CONFIGURATION # +###################################################################### + +# There are no rewriting specifications in this default configuration file. + +begin rewrite + + + +###################################################################### +# AUTHENTICATION CONFIGURATION # +###################################################################### + +# The following authenticators support plaintext username/password +# authentication using the standard PLAIN mechanism and the traditional +# but non-standard LOGIN mechanism, with Exim acting as the server. +# PLAIN and LOGIN are enough to support most MUA software. +# +# These authenticators are not complete: you need to change the +# server_condition settings to specify how passwords are verified. +# They are set up to offer authentication to the client only if the +# connection is encrypted with TLS, so you also need to add support +# for TLS. See the global configuration options section at the start +# of this file for more about TLS. +# +# The default RCPT ACL checks for successful authentication, and will accept +# messages from authenticated users from anywhere on the Internet. + +begin authenticators + +# PLAIN authentication has no server prompts. The client sends its +# credentials in one lump, containing an authorization ID (which we do not +# use), an authentication ID, and a password. The latter two appear as +# $auth2 and $auth3 in the configuration and should be checked against a +# valid username and password. In a real configuration you would typically +# use $auth2 as a lookup key, and compare $auth3 against the result of the +# lookup, perhaps using the crypteq{}{} condition. + +#PLAIN: +# driver = plaintext +# server_set_id = $auth2 +# server_prompts = : +# server_condition = Authentication is not yet configured +# server_advertise_condition = ${if def:tls_in_cipher } + +# LOGIN authentication has traditional prompts and responses. There is no +# authorization ID in this mechanism, so unlike PLAIN the username and +# password are $auth1 and $auth2. Apart from that you can use the same +# server_condition setting for both authenticators. + +#LOGIN: +# driver = plaintext +# server_set_id = $auth1 +# server_prompts = <| Username: | Password: +# server_condition = Authentication is not yet configured +# server_advertise_condition = ${if def:tls_in_cipher } + + +###################################################################### +# CONFIGURATION FOR local_scan() # +###################################################################### + +# If you have built Exim to include a local_scan() function that contains +# tables for private options, you can define those options here. Remember to +# uncomment the "begin" line. It is commented by default because it provokes +# an error with Exim binaries that are not built with LOCAL_SCAN_HAS_OPTIONS +# set in the Local/Makefile. + +# begin local_scan + + +# End of Exim configuration file diff --git a/src/convert4r3.src b/src/convert4r3.src new file mode 100755 index 0000000..d0b94d1 --- /dev/null +++ b/src/convert4r3.src @@ -0,0 +1,1382 @@ +#! PERL_COMMAND + +# This is a Perl script that reads an Exim run-time configuration file and +# checks for settings that were valid prior to release 3.00 but which were +# obsoleted by that release. It writes a new file with suggested changes to +# the standard output, and commentary about what it has done to stderr. + +# It is assumed that the input is a valid Exim configuration file. + +use warnings; +BEGIN { pop @INC if $INC[-1] eq '.' }; + +use Getopt::Long; +use File::Basename; + +GetOptions( + 'version' => sub { + print basename($0) . ": $0\n", + "build: EXIM_RELEASE_VERSIONEXIM_VARIANT_VERSION\n", + "perl(runtime): $^V\n"; + exit 0; + }, +); + +################################################## +# Analyse one line # +################################################## + +# This is called for the main and the driver sections, not for retry +# or rewrite sections (which are unmodified). + +sub checkline{ +my($line) = $_[0]; + +return "comment" if $line =~ /^\s*(#|$)/; +return "end" if $line =~ /^\s*end\s*$/; + +# Macros are recognized only in the first section of the file. + +return "macro" if $prefix eq "" && $line =~ /^\s*[A-Z]/; + +# Pick out the name at the start and the rest of the line (into global +# variables) and return whether the start of a driver or not. + +($i1,$name,$i2,$rest) = $line =~ /^(\s*)([a-z0-9_]+)(\s*)(.*?)\s*$/; +return ($rest =~ /^:/)? "driver" : "option"; +} + + + + +################################################## +# Add transport setting to a director # +################################################## + +# This function adds a transport setting to an aliasfile or forwardfile +# director if a global setting exists and a local one does not. If neither +# exist, it adds file/pipe/reply, but not the directory ones. + +sub add_transport{ +my($option) = @_; + +my($key) = "$prefix$driver.${option}_transport"; +if (!exists $o{$key}) + { + if (exists $o{"address_${option}_transport"}) + { + print STDOUT "# >> Option added by convert4r3\n"; + printf STDOUT "${i1}${option}_transport = %s\n", + $o{"address_${option}_transport"}; + printf STDERR + "\n%03d ${option}_transport added to $driver director.\n", + ++$count; + } + else + { + if ($option eq "pipe" || $option eq "file" || $option eq "reply") + { + print STDOUT "# >> Option added by convert4r3\n"; + printf STDOUT "${i1}${option}_transport = address_${option}\n"; + printf STDERR + "\n%03d ${option}_transport added to $driver director.\n", + ++$count; + } + } + } +} + + + + +################################################## +# Negate a list of things # +################################################## + +sub negate { +my($list) = $_[0]; + +return $list if ! defined $list; + +($list) = $list =~ /^"?(.*?)"?\s*$/s; + +# Under Perl 5.005 we can split very nicely at colons, ignoring double +# colons, like this: +# +# @split = split /\s*(?%%%%%%%%> Option rewritten by convert4r3\n"; +print STDOUT "${i1}$name = \""; + +if (defined $reject_except) + { + print STDOUT "$reject_except"; + $join = " : \\\n "; + $last_was_negated = ($_[4] ne "+"); + } +if (defined $reject) + { + print STDOUT "$join$reject"; + $join = " : \\\n "; + $last_was_negated = ($_[4] eq "+"); + } +if (defined $accept) + { + print STDOUT "$join$accept"; + $last_was_negated = ($_[4] ne "+"); + $join = " : \\\n "; + } + +print STDOUT "$join*" if $last_was_negated; + +print STDOUT "\"\n"; + +my($driver_name); +my($driver_type) = ""; + +if ($_[0] =~ /^(d|r|t)\.([^.]+)\./ || + $_[1] =~ /^(d|r|t)\.([^.]+)\./ || + $_[2] =~ /^(d|r|t)\.([^.]+)\./) + { + $driver_type = ($1 eq 'd')? "director" : ($1 eq 'r')? "router" : "transport"; + $driver_name = $2; + } + +my($x) = ($driver_type ne "")? " in \"$driver_name\" $driver_type" : ""; + +my($l0) = &base($_[0]); +my($l1) = &base($_[1]); +my($l2) = &base($_[2]); + + +if ($l2 eq "") + { + if ($l0 eq "") + { + printf STDERR "\n%03d $l1 converted to $name$x.\n", ++$count; + } + else + { + printf STDERR "\n%03d $l0 and $l1\n amalgamated into $name$x.\n", + ++$count; + } + } +else + { + if ($l1 eq "") + { + printf STDERR "\n%03d $l0 and $l2\n amalgamated into $name$x.\n", + ++$count; + } + else + { + printf STDERR "\n%03d $l0, $l1 and $l2\n amalgamated into " . + "$name$x.\n", ++$count; + } + } +} + + + + +################################################## +# Join two lists, if they exist # +################################################## + +sub pair{ +my($l1) = $o{"$_[0]"}; +my($l2) = $o{"$_[1]"}; + +return $l2 if (!defined $l1); +return $l1 if (!defined $l2); + +($l1) = $l1 =~ /^"?(.*?)"?\s*$/s; +($l2) = $l2 =~ /^"?(.*?)"?\s*$/s; + +return "$l1 : $l2"; +} + + + + +################################################## +# Amalgamate accept/reject/reject_except pairs # +################################################## + +# This is like amalgamate, but it combines pairs of arguments, and +# doesn't output commentary (easier to write a generic one for the few +# cases). + +sub amalgamatepairs { +my($accept) = &pair($_[0], $_[1]); +my($reject) = &pair($_[2], $_[3]); +my($reject_except) = &pair($_[4], $_[5]); +my($last_was_negated) = 0; +my($join) = ""; + +if ($_[7] eq "+") + { + ($accept) = $accept =~ /^"?(.*?)"?\s*$/s if defined $accept; + $reject = &negate($reject) if defined $reject; + ($reject_except) = $reject_except =~ /^"?(.*?)"?\s*$/s if defined $reject_except; + } +else + { + $accept = &negate($accept) if defined $accept; + ($reject) = $reject =~ /^"?(.*?)"?$/s if defined $reject; + $reject_except = &negate($reject_except) if defined $reject_except; + } + +print STDOUT "# >> Option rewritten by convert4r3\n"; +print STDOUT "${i1}$_[6] = \""; + +if (defined $reject_except) + { + print STDOUT "$reject_except"; + $join = " : \\\n "; + $last_was_negated = ($_[7] ne "+"); + } +if (defined $reject) + { + print STDOUT "$join$reject"; + $join = " : \\\n "; + $last_was_negated = ($_[7] eq "+"); + } +if (defined $accept) + { + print STDOUT "$join$accept"; + $last_was_negated = ($_[7] ne "+"); + $join = " : \\\n "; + } + +print STDOUT "$join*" if $last_was_negated; +print STDOUT "\"\n"; +} + + + +################################################## +# Amalgamate boolean and exception list(s) # +################################################## + +sub amalgboolandlist { +my($name,$bool,$e1,$e2) = @_; + +print STDOUT "# >> Option rewritten by convert4r3\n"; +if ($bool eq "false") + { + printf STDOUT "$i1$name =\n"; + } +else + { + printf STDOUT "$i1$name = "; + my($n1) = &negate($o{$e1}); + my($n2) = &negate($o{$e2}); + if (!defined $n1 && !defined $n2) + { + print STDOUT "*\n"; + } + elsif (!defined $n1) + { + print STDOUT "\"$n2 : \\\n *\"\n"; + } + elsif (!defined $n2) + { + print STDOUT "\"$n1 : \\\n *\"\n"; + } + else + { + print STDOUT "\"$n1 : \\\n $n2 : \\\n *\"\n"; + } + } +} + + + +################################################## +# Convert mask format # +################################################## + +# This function converts an address and mask in old-fashioned dotted-quad +# format into an address plus a new format mask. + +@byte_list = (0, 128, 192, 224, 240, 248, 252, 254, 255); + +sub mask { +my($address,$mask) = @_; +my($length) = 0; +my($i, $j); + +my(@bytes) = split /\./, $mask; + +for ($i = 0; $i < 4; $i++) + { + for ($j = 0; $j <= 8; $j++) + { + if ($bytes[$i] == $byte_list[$j]) + { + $length += $j; + if ($j != 8) + { + for ($i++; $i < 4; $i++) + { + $j = 9 if ($bytes[$i] != 0); + } + } + last; + } + } + + if ($j > 8) + { + print STDERR "*** IP mask $mask cannot be converted to /n format. ***\n"; + return "$address/$mask"; + } + } + +if (!defined $masks{$mask}) + { + printf STDERR "\n%03d IP address mask $mask converted to /$length\n", + ++$count, $mask, $length; + $masks{$mask} = 1; + } + +return sprintf "$address/%d", $length; +} + + + + + +################################################## +# Main program # +################################################## + +print STDERR "Exim pre-release 3.00 configuration file converter.\n"; + +$count = 0; +$seen_helo_accept_junk = 0; +$seen_hold_domains = 0; +$seen_receiver_unqualified = 0; +$seen_receiver_verify_except = 0; +$seen_receiver_verify_senders = 0; +$seen_rfc1413_except = 0; +$seen_sender_accept = 0; +$seen_sender_accept_recipients = 0; +$seen_sender_host_accept = 0; +$seen_sender_host_accept_recipients = 0; +$seen_sender_host_accept_relay = 0; +$seen_sender_unqualified = 0; +$seen_sender_verify_except_hosts = 0; +$seen_smtp_etrn = 0; +$seen_smtp_expn = 0; +$seen_smtp_reserve = 0; +$semicomma = 0; + +# Read the entire file into an array + +chomp(@c = ); + +# First, go through the input and covert any net masks in the old dotted-quad +# style into the new /n style. + +for ($i = 0; $i < scalar(@c); $i++) + { + $c[$i] =~ + s"((?:\d{1,3}\.){3}\d{1,3})/((?:\d{1,3}\.){3}\d{1,3})"&mask($1,$2)"eg; + } + +# We now make two more passes over the input. In the first pass, we place all +# the option values into an associative array. Main options are keyed by their +# names; options for drivers are keyed by a driver type letter, the driver +# name, and the option name, dot-separated. In the second pass we modify +# the options if necessary, and write the output file. + +for ($pass = 1; $pass < 3; $pass++) + { + $prefix = ""; + $driver = ""; + $last_was_blank = 0; + + for ($i = 0; $i < scalar(@c); $i++) + { + # Everything after the router section is just copied in pass 2 and + # ignored in pass 1. + + if ($prefix eq "end") + { + print STDOUT "$c[$i]\n" if $pass == 2; + next; + } + + # Analyze the line + + $type = &checkline($c[$i]); + + # Skip comments in pass 1; copy in pass 2 + + if ($type eq "comment") + { + $last_was_blank = ($c[$i] =~ /^\s*$/)? 1 : 0; + print STDOUT "$c[$i]\n" if $pass == 2; + next; + } + + # Skip/copy macro definitions, but must handle continuations + + if ($type eq "macro") + { + print STDOUT "$c[$i]\n" if $pass == 2; + while ($c[$i] =~ /\\\s*$/) + { + $i++; + print STDOUT "$c[$i]\n" if $pass == 2; + } + $last_was_blank = 0; + next; + } + + # Handle end of section + + if ($type eq "end") + { + $prefix = "end"if $prefix eq "r."; + $prefix = "r." if $prefix eq "d."; + $prefix = "d." if $prefix eq "t."; + $prefix = "t." if $prefix eq ""; + print STDOUT "$c[$i]\n" if $pass == 2; + $last_was_blank = 0; + next; + } + + # Handle start of a new driver + + if ($type eq "driver") + { + $driver = $name; + print STDOUT "$c[$i]\n" if $pass == 2; + $last_was_blank = 0; + $seen_domains = 0; + $seen_local_parts = 0; + $seen_senders = 0; + $seen_mx_domains = 0; + $seen_serialize = 0; + next; + } + + # Handle definition of an option + + if ($type eq "option") + { + # Handle continued strings + + if ($rest =~ /^=\s*".*\\$/) + { + for (;;) + { + $rest .= "\n$c[++$i]"; + last unless $c[$i] =~ /(\\\s*$|^\s*#)/; + } + } + + # Remove any terminating commas and semicolons in pass 2 + + if ($pass == 2 && $rest =~ /[;,]\s*$/) + { + $rest =~ s/\s*[;,]\s*$//; + if (!$semicomma) + { + printf STDERR + "\n%03d Terminating semicolons and commas removed from driver " . + "options.\n", ++$count; + $semicomma = 1; + } + } + + # Convert all booleans to "x = true/false" format, but save the + # original so that it can be reproduced unchanged for options that + # are not of interest. + + $origname = $name; + $origrest = $rest; + + if ($name =~ /^not?_(.*)/) + { + $name = $1; + $rest = "= false"; + } + elsif ($rest !~ /^=/) + { + $rest = "= true"; + } + + # Set up the associative array key, and get rid of the = on the data + + $key = ($prefix eq "")? "$name" : "$prefix$driver.$name"; + ($rest) = $rest =~ /^=\s*(.*)/s; + + # Create the associative array of values in pass 1 + + if ($pass == 1) + { + $o{$key} = $rest; + } + + # In pass 2, test for interesting options and do the necessary; copy + # all the rest. + + else + { + ########## Global configuration ########## + + # These global options are abolished + + if ($name eq "address_directory_transport" || + $name eq "address_directory2_transport" || + $name eq "address_file_transport" || + $name eq "address_pipe_transport" || + $name eq "address_reply_transport") + { + ($n2) = $name =~ /^address_(.*)/; + printf STDERR "\n%03d $name option deleted.\n", ++$count; + printf STDERR " $n2 will be added to appropriate directors.\n"; + $i = &skipblanks($i); + next; + } + + # This debugging option is abolished + + elsif ($name eq "sender_verify_log_details") + { + printf STDERR "\n%03d $name option deleted.\n", ++$count; + printf STDERR " (Little used facility abolished.)\n"; + } + + # This option has been renamed + + elsif ($name eq "check_dns_names") + { + $origname =~ s/check_dns/dns_check/; + print STDOUT "# >> Option rewritten by convert4r3\n"; + print STDOUT "$i1$origname$i2$origrest\n"; + printf STDERR "\n%03d check_dns_names renamed as dns_check_names.\n", + ++$count; + } + + # helo_accept_junk_nets is abolished + + elsif ($name eq "helo_accept_junk_nets" || + $name eq "helo_accept_junk_hosts") + { + if (!$seen_helo_accept_junk) + { + &amalgamate("helo_accept_junk_nets", "", + "helo_accept_junk_hosts", "helo_accept_junk_hosts", "+"); + $seen_helo_accept_junk = 1; + } + else + { + $i = &skipblanks($i); + next; + } + } + + # helo_verify_except_{hosts,nets} are abolished, and helo_verify + # is now a host list instead of a boolean. + + elsif ($name eq "helo_verify") + { + &amalgboolandlist("helo_verify", $rest, "helo_verify_except_hosts", + "helo_verify_except_nets"); + printf STDERR "\n%03d helo_verify converted to host list.\n", + ++$count; + } + elsif ($name eq "helo_verify_except_hosts" || + $name eq "helo_verify_except_nets") + { + $i = &skipblanks($i); + next; + } + + # helo_verify_nets was an old synonym for host_lookup_nets; only + # one of them will be encountered. Change to a new name. + + elsif ($name eq "helo_verify_nets" || + $name eq "host_lookup_nets") + { + print STDOUT "# >> Option rewritten by convert4r3\n"; + print STDOUT "${i1}host_lookup$i2$origrest\n"; + printf STDERR "\n%03d $name renamed as host_lookup.\n", ++$count; + } + + # hold_domains_except is abolished; add as negated items to + # hold_domains. + + elsif ($name eq "hold_domains_except" || + $name eq "hold_domains") + { + if ($seen_hold_domains) # If already done with these + { # omit, and following blanks. + $i = &skipblanks($i); + next; + } + $seen_hold_domains = 1; + + if (exists $o{"hold_domains_except"}) + { + &amalgamate("hold_domains", "hold_domains_except", "", + "hold_domains", "+"); + } + else + { + print STDOUT "$i1$origname$i2$origrest\n"; + } + } + + # ignore_fromline_nets is renamed as ignore_fromline_hosts + + elsif ($name eq "ignore_fromline_nets") + { + $origname =~ s/_nets/_hosts/; + print STDOUT "# >> Option rewritten by convert4r3\n"; + print STDOUT "$i1$origname$i2$origrest\n"; + printf STDERR + "\n%03d ignore_fromline_nets renamed as ignore_fromline_hosts.\n", + ++$count; + } + + # Output a warning for message filters with no transports set + + elsif ($name eq "message_filter") + { + print STDOUT "$i1$origname$i2$origrest\n"; + + if (!exists $o{"message_filter_directory_transport"} && + !exists $o{"message_filter_directory2_transport"} && + !exists $o{"message_filter_file_transport"} && + !exists $o{"message_filter_pipe_transport"} && + !exists $o{"message_filter_reply_transport"}) + { + printf STDERR + "\n%03d message_filter is set, but no message_filter transports " + . "are defined.\n" + . " If your filter generates file or pipe deliveries, or " + . "auto-replies,\n" + . " you will need to define " + . "message_filter_{file,pipe,reply}_transport\n" + . " options, as required.\n", ++$count; + } + } + + # queue_remote_except is abolished, and queue_remote is replaced by + # queue_remote_domains, which is a host list. + + elsif ($name eq "queue_remote") + { + &amalgboolandlist("queue_remote_domains", $rest, + "queue_remote_except", ""); + printf STDERR + "\n%03d queue_remote converted to domain list queue_remote_domains.\n", + ++$count; + } + elsif ($name eq "queue_remote_except") + { + $i = &skipblanks($i); + next; + } + + # queue_smtp_except is abolished, and queue_smtp is replaced by + # queue_smtp_domains, which is a host list. + + elsif ($name eq "queue_smtp") + { + &amalgboolandlist("queue_smtp_domains", $rest, + "queue_smtp_except", ""); + printf STDERR + "\n%03d queue_smtp converted to domain list queue_smtp_domains.\n", + ++$count; + } + elsif ($name eq "queue_smtp_except") + { + $i = &skipblanks($i); + next; + } + + # rbl_except_nets is replaced by rbl_hosts + + elsif ($name eq "rbl_except_nets") + { + &amalgamate("", "rbl_except_nets", "", "rbl_hosts", "+"); + } + + # receiver_unqualified_nets is abolished + + elsif ($name eq "receiver_unqualified_nets" || + $name eq "receiver_unqualified_hosts") + { + if (!$seen_receiver_unqualified) + { + &amalgamate("receiver_unqualified_nets", "", + "receiver_unqualified_hosts", "receiver_unqualified_hosts", "+"); + $seen_receiver_unqualified = 1; + } + else + { + $i = &skipblanks($i); + next; + } + } + + # receiver_verify_except_{hosts,nets} are replaced by + # receiver_verify_hosts. + + elsif ($name eq "receiver_verify_except_hosts" || + $name eq "receiver_verify_except_nets") + { + if (!$seen_receiver_verify_except) + { + &amalgboolandlist("receiver_verify_hosts", "true", + "receiver_verify_except_hosts", "receiver_verify_except_nets"); + printf STDERR + "\n%03d receiver_verify_except_{hosts,nets} converted to " . + "receiver_verify_hosts.\n", + ++$count; + $seen_receiver_verify_except = 1; + } + else + { + $i = &skipblanks($i); + next; + } + } + + # receiver_verify_senders_except is abolished + + elsif ($name eq "receiver_verify_senders" || + $name eq "receiver_verify_senders_except") + { + if (defined $o{"receiver_verify_senders_except"}) + { + if (!$seen_receiver_verify_senders) + { + &amalgamate("receiver_verify_senders", + "receiver_verify_senders_except", "", + "receiver_verify_senders", "+"); + $seen_receiver_verify_senders = 1; + } + else + { + $i = &skipblanks($i); + next; + } + } + else + { + print STDOUT "$i1$origname$i2$origrest\n"; + } + } + + # rfc1413_except_{hosts,nets} are replaced by rfc1413_hosts. + + elsif ($name eq "rfc1413_except_hosts" || + $name eq "rfc1413_except_nets") + { + if (!$seen_rfc1413_except) + { + &amalgboolandlist("rfc1413_hosts", "true", + "rfc1413_except_hosts", "rfc1413_except_nets"); + printf STDERR + "\n%03d rfc1413_except_{hosts,nets} converted to rfc1413_hosts.\n", + ++$count; + $seen_rfc1413_except = 1; + } + else + { + $i = &skipblanks($i); + next; + } + } + + # sender_accept and sender_reject_except are abolished + + elsif ($name eq "sender_accept" || + $name eq "sender_reject") + { + if (!$seen_sender_accept) + { + &amalgamate("sender_accept", "sender_reject", + "sender_reject_except", "sender_reject", "-"); + $seen_sender_accept = 1; + } + else + { + $i = &skipblanks($i); + next; + } + } + + # sender_accept_recipients is also abolished; sender_reject_except + # also used to apply to this, so we include it here as well. + + elsif ($name eq "sender_accept_recipients" || + $name eq "sender_reject_recipients") + { + if (!$seen_sender_accept_recipients) + { + &amalgamate("sender_accept_recipients", "sender_reject_recipients", + "sender_reject_except", "sender_reject_recipients", "-"); + $seen_sender_accept_recipients = 1; + } + else + { + $i = &skipblanks($i); + next; + } + } + + # sender_reject_except must be removed + + elsif ($name eq "sender_reject_except") + { + $i = &skipblanks($i); + next; + } + + # sender_{host,net}_{accept,reject}[_except] all collapse into + # host_reject. + + elsif ($name eq "sender_host_accept" || + $name eq "sender_net_accept" || + $name eq "sender_host_reject" || + $name eq "sender_net_reject") + { + if (!$seen_sender_host_accept) + { + &amalgamatepairs("sender_host_accept", "sender_net_accept", + "sender_host_reject", "sender_net_reject", + "sender_host_reject_except", "sender_net_reject_except", + "host_reject", "-"); + printf STDERR "\n%03d sender_{host,net}_{accept,reject} and " . + "sender_{host_net}_reject_except\n" . + " amalgamated into host_reject.\n", ++$count; + $seen_sender_host_accept = 1; + } + else + { + $i = &skipblanks($i); + next; + } + } + + # sender_{host,net}_{accept,reject}_recipients all collapse into + # host_reject_recipients. + + elsif ($name eq "sender_host_accept_recipients" || + $name eq "sender_net_accept_recipients" || + $name eq "sender_host_reject_recipients" || + $name eq "sender_net_reject_recipients") + { + if (!$seen_sender_host_accept_recipients) + { + &amalgamatepairs("sender_host_accept_recipients", + "sender_net_accept_recipients", + "sender_host_reject_recipients", + "sender_net_reject_recipients", + "sender_host_reject_except", "sender_net_reject_except", + "host_reject_recipients", "-"); + printf STDERR "\n%03d sender_{host,net}_{accept,reject}_recipients" + . "\n and sender_{host_net}_reject_except" + . "\n amalgamated into host_reject_recipients.\n", ++$count; + $seen_sender_host_accept_recipients = 1; + } + else + { + $i = &skipblanks($i); + next; + } + } + + # sender_{host,net}_reject_except must be removed + + elsif ($name eq "sender_host_reject_except" || + $name eq "sender_net_reject_except") + { + $i = &skipblanks($i); + next; + } + + # sender_{host,net}_{accept,reject}_relay all collapse into + # host_accept_relay. + + elsif ($name eq "sender_host_accept_relay" || + $name eq "sender_net_accept_relay" || + $name eq "sender_host_reject_relay" || + $name eq "sender_net_reject_relay") + { + if (!$seen_sender_host_accept_relay) + { + &amalgamatepairs("sender_host_accept_relay", + "sender_net_accept_relay", + "sender_host_reject_relay", + "sender_net_reject_relay", + "sender_host_reject_relay_except", + "sender_net_reject_relay_except", + "host_accept_relay", "+"); + printf STDERR "\n%03d sender_{host,net}_{accept,reject}_relay" + . "\n and sender_{host_net}_reject_relay_except" + . "\n amalgamated into host_accept_relay.\n", ++$count; + $seen_sender_host_accept_relay = 1; + } + else + { + $i = &skipblanks($i); + next; + } + } + + # sender_{host,net}_reject_relay_except must be removed + + elsif ($name eq "sender_host_reject_relay_except" || + $name eq "sender_net_reject_relay_except") + { + $i = &skipblanks($i); + next; + } + + + # sender_unqualified_nets is abolished + + elsif ($name eq "sender_unqualified_nets" || + $name eq "sender_unqualified_hosts") + { + if (!$seen_sender_unqualified) + { + &amalgamate("sender_unqualified_nets", "", + "sender_unqualified_hosts", "sender_unqualified_hosts", "+"); + $seen_sender_unqualified = 1; + } + else + { + $i = &skipblanks($i); + next; + } + } + + # sender_verify_except_{hosts,nets} are replaced by sender_verify_hosts. + + elsif ($name eq "sender_verify_except_hosts" || + $name eq "sender_verify_except_nets") + { + if (!$seen_sender_verify_except_hosts) + { + &amalgboolandlist("sender_verify_hosts", "true", + "sender_verify_except_hosts", "sender_verify_except_nets"); + printf STDERR + "\n%03d sender_verify_except_{hosts,nets} converted to " . + "sender_verify_hosts.\n", + ++$count; + $seen_sender_verify_except_hosts = 1; + } + else + { + $i = &skipblanks($i); + next; + } + } + + # smtp_etrn_nets is abolished + + elsif ($name eq "smtp_etrn_nets" || + $name eq "smtp_etrn_hosts") + { + if (!$seen_smtp_etrn) + { + &amalgamate("smtp_etrn_nets", "", + "smtp_etrn_hosts", "smtp_etrn_hosts", "+"); + $seen_smtp_etrn = 1; + } + else + { + $i = &skipblanks($i); + next; + } + } + + # smtp_expn_nets is abolished + + elsif ($name eq "smtp_expn_nets" || + $name eq "smtp_expn_hosts") + { + if (!$seen_smtp_expn) + { + &amalgamate("smtp_expn_nets", "", + "smtp_expn_hosts", "smtp_expn_hosts", "+"); + $seen_smtp_expn = 1; + } + else + { + $i = &skipblanks($i); + next; + } + } + + # This option has been renamed + + elsif ($name eq "smtp_log_connections") + { + $origname =~ s/smtp_log/log_smtp/; + print STDOUT "# >> Option rewritten by convert4r3\n"; + print STDOUT "$i1$origname$i2$origrest\n"; + printf STDERR "\n%03d smtp_log_connections renamed as " . + "log_smtp_connections.\n", + ++$count; + } + + # smtp_reserve_nets is abolished + + elsif ($name eq "smtp_reserve_nets" || + $name eq "smtp_reserve_hosts") + { + if (!$seen_smtp_reserve) + { + &amalgamate("smtp_reserve_nets", "", + "smtp_reserve_hosts", "smtp_reserve_hosts", "+"); + $seen_smtp_reserve = 1; + } + else + { + $i = &skipblanks($i); + next; + } + } + + ########### Driver configurations ########## + + # For aliasfile and forwardfile directors, add file, pipe, and + # reply transports - copying from the globals if they are set. + + elsif ($name eq "driver") + { + $driver_type = $rest; + print STDOUT "$i1$origname$i2$origrest\n"; + if ($rest eq "aliasfile" || $rest eq "forwardfile") + { + &add_transport("directory"); + &add_transport("directory2"); + &add_transport("file"); + &add_transport("pipe"); + &add_transport("reply") if $rest eq "forwardfile"; + } + } + + # except_domains is abolished; add as negated items to domains. + + elsif ($name eq "except_domains" || + $name eq "domains") + { + if ($seen_domains) # If already done with these + { # omit, and following blanks. + $i = &skipblanks($i); + next; + } + $seen_domains = 1; + + if (exists $o{"$prefix$driver.except_domains"}) + { + &amalgamate("$prefix$driver.domains", + "$prefix$driver.except_domains", "", + "domains", "+"); + } + else + { + print STDOUT "$i1$origname$i2$origrest\n"; + } + } + + # except_local_parts is abolished; add as negated items to + # local_parts. + + elsif ($name eq "except_local_parts" || + $name eq "local_parts") + { + if ($seen_local_parts) # If already done with these + { # omit, and following blanks. + $i = &skipblanks($i); + next; + } + $seen_local_parts = 1; + + if (exists $o{"$prefix$driver.except_local_parts"}) + { + &amalgamate("$prefix$driver.local_parts", + "$prefix$driver.except_local_parts", "", + "local_parts", "+"); + } + else + { + print STDOUT "$i1$origname$i2$origrest\n"; + } + } + + # except_senders is abolished; add as negated items to senders + + elsif ($name eq "except_senders" || + $name eq "senders") + { + if ($seen_senders) # If already done with these + { # omit, and following blanks. + $i = &skipblanks($i); + next; + } + $seen_senders = 1; + + if (exists $o{"$prefix$driver.except_senders"}) + { + &amalgamate("$prefix$driver.senders", + "$prefix$driver.except_senders", "", + "senders", "+"); + } + else + { + print STDOUT "$i1$origname$i2$origrest\n"; + } + } + + # This option has been renamed + + elsif ($name eq "directory" && $driver_type eq "aliasfile") + { + $origname =~ s/directory/home_directory/; + print STDOUT "# >> Option rewritten by convert4r3\n"; + print STDOUT "$i1$origname$i2$origrest\n"; + printf STDERR "\n%03d directory renamed as " . + "home_directory in \"$driver\" director.\n", + ++$count; + } + + # This option has been renamed + + elsif ($name eq "directory" && $driver_type eq "forwardfile") + { + $origname =~ s/directory/file_directory/; + print STDOUT "# >> Option rewritten by convert4r3\n"; + print STDOUT "$i1$origname$i2$origrest\n"; + printf STDERR "\n%03d directory renamed as " . + "file_directory in \"$driver\" director.\n", + ++$count; + } + + # This option has been renamed + + elsif ($name eq "forbid_filter_log" && $driver_type eq "forwardfile") + { + $origname =~ s/log/logwrite/; + print STDOUT "# >> Option rewritten by convert4r3\n"; + print STDOUT "$i1$origname$i2$origrest\n"; + printf STDERR "\n%03d forbid_filter_log renamed as " . + "forbid_filter_logwrite in \"$driver\" director.\n", + ++$count; + } + + # This option has been renamed + + elsif ($name eq "directory" && $driver_type eq "localuser") + { + $origname =~ s/directory/match_directory/; + print STDOUT "# >> Option rewritten by convert4r3\n"; + print STDOUT "$i1$origname$i2$origrest\n"; + printf STDERR "\n%03d directory renamed as " . + "match_directory in \"$driver\" director.\n", + ++$count; + } + + # mx_domains_except (and old synonym non_mx_domains) are abolished + # (both lookuphost router and smtp transport) + + elsif ($name eq "mx_domains" || + $name eq "mx_domains_except" || + $name eq "non_mx_domains") + { + if ($seen_mx_domains) # If already done with these + { # omit, and following blanks. + $i = &skipblanks($i); + next; + } + $seen_mx_domains = 1; + + if (exists $o{"$prefix$driver.mx_domains_except"} || + exists $o{"$prefix$driver.non_mx_domains"}) + { + $o{"$prefix$driver.mx_domains_except"} = + &pair("$prefix$driver.mx_domains_except", + "$prefix$driver.non_mx_domains"); + + &amalgamate("$prefix$driver.mx_domains", + "$prefix$driver.mx_domains_except", "", + "mx_domains", "+"); + } + else + { + print STDOUT "$i1$origname$i2$origrest\n"; + } + } + + # This option has been renamed + + elsif ($name eq "directory" && $driver_type eq "pipe") + { + $origname =~ s/directory/home_directory/; + print STDOUT "# >> Option rewritten by convert4r3\n"; + print STDOUT "$i1$origname$i2$origrest\n"; + printf STDERR "\n%03d directory renamed as " . + "home_directory in \"$driver\" director.\n", + ++$count; + } + + # serialize_nets is abolished + + elsif ($name eq "serialize_nets" || + $name eq "serialize_hosts") + { + if (!$seen_serialize) + { + &amalgamate("$prefix$driver.serialize_nets", "", + "$prefix$driver.serialize_hosts", "serialize_hosts", "+"); + $seen_serialize = 1; + } + else + { + $i = &skipblanks($i); + next; + } + } + + + # Option not of interest; reproduce verbatim + + else + { + print STDOUT "$i1$origname$i2$origrest\n"; + } + + + $last_was_blank = 0; + } + } + } + + } + +# Debugging: show the associative array +# foreach $key (sort keys %o) { print STDERR "$key = $o{$key}\n"; } + +print STDERR "\nEnd of configuration file conversion.\n"; +print STDERR "\n*******************************************************\n"; +print STDERR "***** Please review the generated file carefully. *****\n"; +print STDERR "*******************************************************\n\n"; + +print STDERR "In particular:\n\n"; + +print STDERR "(1) If you use regular expressions in any options that have\n"; +print STDERR " been rewritten by this script, they might have been put\n"; +print STDERR " inside quotes, when then were not previously quoted. This\n"; +print STDERR " means that any backslashes in them must now be escaped.\n\n"; + +print STDERR "(2) If your configuration refers to any external files that\n"; +print STDERR " contain lists of network addresses, check that the masks\n"; +print STDERR " are specified as single numbers, e.g. /24 and NOT as dotted\n"; +print STDERR " quads (e.g. 255.255.255.0) because Exim release 3.00 does\n"; +print STDERR " not recognize the dotted quad form.\n\n"; + +print STDERR "(3) If your configuration uses macros for lists of domains or\n"; +print STDERR " hosts or addresses, check to see if any of the references\n"; +print STDERR " have been negated. If so, you will have to rework things,\n"; +print STDERR " because the negation will apply only to the first item in\n"; +print STDERR " the macro-generated list.\n\n"; + +print STDERR "(4) If you do not generate deliveries to pipes, files, or\n"; +print STDERR " auto-replies in your aliasfile and forwardfile directors,\n"; +print STDERR " you can remove the added transport settings.\n\n"; + +# End of convert4r3 diff --git a/src/convert4r4.src b/src/convert4r4.src new file mode 100755 index 0000000..47987fc --- /dev/null +++ b/src/convert4r4.src @@ -0,0 +1,2527 @@ +#! PERL_COMMAND + +# This is a Perl script that reads an Exim run-time configuration file for +# Exim 3. It makes what changes it can for Exim 4, and also output commentary +# on what it has done, and on things it cannot do. + +# It is assumed that the input is a valid Exim 3 configuration file. + +use warnings; +BEGIN { pop @INC if $INC[-1] eq '.' }; + +use Getopt::Long; +use File::Basename; + +GetOptions( + 'version' => sub { + print basename($0) . ": $0\n", + "build: EXIM_RELEASE_VERSIONEXIM_VARIANT_VERSION\n", + "perl(runtime): $^V\n"; + exit 0; + }, +); + +# These are lists of main options which are abolished in Exim 4. +# The first contains options that are used to construct new options. + +@skipped_options = ( +"auth_hosts", +"auth_over_tls_hosts", +"errors_address", +"headers_check_syntax", +"headers_checks_fail", +"headers_sender_verify", +"headers_sender_verify_errmsg", +"host_accept_relay", +"host_auth_accept_relay", +"host_reject_recipients", +"local_domains", +"local_domains_include_host", +"local_domains_include_host_literals", +"log_all_parents", +"log_arguments", +"log_incoming_port", +"log_interface", +"log_level", +"log_received_sender", +"log_received_recipients", +"log_rewrites", +"log_sender_on_delivery", +"log_smtp_confirmation", +"log_smtp_connections", +"log_smtp_syntax_errors", +"log_subject", +"log_queue_run_level", +"rbl_domains", +"rbl_hosts", +"rbl_reject_recipients", +"receiver_verify", +"receiver_verify_addresses", +"receiver_verify_hosts", +"receiver_verify_senders", +"recipients_reject_except", +"recipients_reject_except_senders", +"relay_domains", +"relay_domains_include_local_mx", +"sender_address_relay", +"sender_address_relay_hosts", +"sender_reject_recipients", +"sender_verify", +"sender_verify_hosts_callback", +"sender_verify_callback_domains", +"sender_verify_callback_timeout", +"sender_verify_hosts", +"smtp_etrn_hosts", +"smtp_expn_hosts", +"smtp_verify", +"tls_host_accept_relay", +"tls_hosts", +"tls_log_cipher", +"tls_log_peerdn", +"tls_verify_ciphers" +); + +# The second contains options that are completely abolished and have +# no equivalent. + +@abolished_options = ( +"always_bcc", +"debug_level", +"helo_strict_syntax", +"kill_ip_options", +"log_ip_options", +"log_refused_recipients", +"message_size_limit_count_recipients", +"rbl_log_headers", +"rbl_log_rcpt_count", +"receiver_try_verify", +"refuse_ip_options", +"relay_match_host_or_sender", +"sender_try_verify", +"sender_verify_batch", +"sender_verify_fixup", +"sender_verify_reject", +"sender_verify_max_retry_rate", +); + +# This is a list of options that are not otherwise handled, but which +# contain domain or host lists that have to be processed so that any +# regular expressions are marked "not for expansion". + +@list_options = ( +"dns_again_means_nonexist", +"hold_domains", +"hosts_treat_as_local", +"percent_hack_domains", +"queue_smtp_domains", +"helo_accept_junk_hosts", +"host_lookup", +"ignore_fromline_hosts", +"rfc1413_hosts", +"sender_unqualified_hosts", +"smtp_reserve_hosts", +"tls_advertise_hosts", +"tls_verify_hosts", +); + + + +################################################## +# Output problem rubric once # +################################################## + +sub rubric { +return if $rubric_output; +$rubric_output = 1; +print STDERR "\n" . +"** The following comments describe problems that have been encountered\n" . +" while converting an Exim 3 runtime file for Exim 4. More detail can\n" . +" be found in the file doc/Exim4.upgrade.\n"; +} + + +################################################## +# Analyse one line # +################################################## + +sub checkline{ +my($line) = $_[0]; + +return "comment" if $line =~ /^\s*(#|$)/; +return "end" if $line =~ /^\s*end\s*$/i; + +# Macros are recognized only in the first section of the file. + +return "macro" if $prefix eq "" && $line =~ /^\s*[A-Z]/; + +# In retry and rewrite sections, the type is always "other" + +return "other" if $prefix eq "=retry" || $prefix eq "=rewrite"; + +# Pick out the name at the start and the rest of the line (into global +# variables) and return whether the start of a driver or not. + +($hide,$name,$rest) = $line =~ /^\s*(hide\s+|)([a-z0-9_]+)\s*(.*?)\s*$/; + +# If $rest begins with a colon, this is a driver name + +return "driver" if $rest =~ /^:/; + +# If $rest begins with an = the value of the option is given explicitly; +# remove the = from the start. Turn "yes"/"no" into "true"/"false". + +if ($rest =~ /^=/) + { + $rest =~ s/^=\s*//; + $rest = "true" if $rest eq "yes"; + $rest = "false" if $rest eq "no"; + } + +# Otherwise we have a boolean option. Set up a "true"/"false" value. + +else + { + if ($name =~ /^not?_/) # Recognize "no_" or "not_" + { + $rest = "false"; + $name =~ s/^not?_//; + } + else + { + $rest = "true"; + } + } + +return "option"; +} + + + +################################################## +# Negate a list of things # +################################################## + +# Can be tricky, because there may be comment lines in the list. +# Also, lists may have different delimiters. + +sub negate { +my($list) = $_[0]; +my($delim) = ":"; +my($leadin) = ""; + +return $list if ! defined $list; + +($list) = $list =~ /^"?(.*?)"?\s*$/s; # Remove surrounding quotes +$list =~ s/\\\s*\n\s*//g; # Remove continuation markers + +if ($list =~ /^(\s*<(\S)\s*)(.*)/s) + { + $leadin = $1; + $delim = $2; + $list = $3; + } + +$list =~ s/^\s+//; +$list =~ s/\Q$delim$delim/>%%%%%%%%%%%%%%%%%%%%%%%%); +$clen = scalar @c; + +# Remove the standard comment that appears at the end of the default + +if ($clen > 0 && $c[$clen-1] =~ /^#\s*End of Exim configuration file\s*/i) + { + pop @c; + $clen--; + } + +# The first pass over the input fishes out all the options settings in the +# main, transport, director, and router sections, and places their values in +# associative arrays. It also notes the starting position of all the sections. + +$prefix = ""; +%main = (); +$hash = \%main; + +for ($i = 0; $i < $clen; $i++) + { + # Change references to +allow_unknown and +warn_unknown into +include_unknown + + if ($c[$i] =~ /\+(?:allow|warn)_unknown/) + { + if (!$unk_output) + { + &rubric(); + print STDERR "\n" . +"** You have used '+allow_unknown' or '+warn_unknown' in a configuration\n" . +" option. This has been converted to '+include_unknown', but the action\n" . +" is different in Exim 4, so you should review all the relevant options.\n"; + $unk_output = 1; + } + $c[$i] =~ s/\+(?:allow|warn)_unknown/+include_unknown/g; + } + + # Any reference to $errmsg_recipient is changed to $bounce_recipient + + if ($c[$i] =~ /\$errmsg_recipient/) + { + if (!$errmsg_output) + { + &rubric(); + print STDERR "\n" . +"** References to \$errmsg_recipient have been changed to \$bounce_recipient\n"; + $errmsg_output = 1; + } + $c[$i] =~ s/\$errmsg_recipient/\$bounce_recipient/g; + } + + + # Analyse the type of line + + $type = &checkline($c[$i]); + next if $type eq "comment"; + + # Output a warning if $key is used + + if ($c[$i] =~ /\$key/ && !$key_output) + { + &rubric(); + print STDERR "\n" . +"** You have used '\$key' in a configuration option. This variable does not\n" . +" exist in Exim 4. Instead, the value you need for your lookup will be\n" . +" in one of the other variables such as '\$domain' or '\$host'. You will\n" . +" need to edit the new configuration to sort this out.\n"; + $key_output = 1; + } + + # Save macro definitions so we can output them first; must handle + # continuations. + + if ($type eq "macro") + { + $macro_output .= "$c[$i++]\n" while $c[$i] =~ /\\\s*$|^\s*#/; + $macro_output .= "$c[$i]\n"; + } + + # Handle end of section + + elsif ($type eq "end") + { + if ($prefix eq "=rewrite") + { + $prefix = "a."; + $auth_start = $i + 1; + last; + } + elsif ($prefix eq "=retry") + { + $prefix = "=rewrite"; + $rewrite_start = $i + 1; + } + elsif ($prefix eq "r.") + { + $prefix = "=retry"; + $retry_start = $i + 1; + } + elsif ($prefix eq "d.") + { + $prefix = "r."; + $router_start = $i + 1; + } + elsif ($prefix eq "t.") + { + $prefix = "d."; + $director_start = $i + 1; + } + elsif ($prefix eq "") + { + $prefix = "t."; + $transport_start = $i + 1; + } + } + + # Handle start of a new director, router or transport driver + + elsif ($type eq "driver" && $prefix !~ /^=/) + { + $hash = {}; + if (defined $driverlist{"$prefix$name"}) + { + die "*** There are two drivers with the name \"$name\"\n"; + } + $driverlist{"$prefix$name"} = $hash; + $first_director = $name if !defined $first_director && $prefix eq "d."; + } + + # Handle definition of an option; we must pull in any continuation + # strings, and save the value in the current hash. Note if the option + # is hidden. + + elsif ($type eq "option") + { + my($nextline) = ""; + + while ($i < $clen - 1 && ($rest =~ /\\\s*$/s || $nextline =~ /^\s*#/)) + { + $nextline = $c[++$i]; + $rest .= "\n$nextline"; + } + + $$hash{$name} = $rest; + $$hash{"$name-hide"} = 1 if $hide ne ""; + } + } + + +# Generate the new configuration. Start with a warning rubric. + +print STDOUT "#!!# This file is output from the convert4r4 script, which tries\n"; +print STDOUT "#!!# to convert Exim 3 configurations into Exim 4 configurations.\n"; +print STDOUT "#!!# However, it is not perfect, especially with non-simple\n"; +print STDOUT "#!!# configurations. You must check it before running it.\n"; +print STDOUT "\n\n"; + +# Output the macro definitions + +if ($macro_output ne "") + { + print STDOUT "#!!# All macro definitions have been gathered here to ensure\n"; + print STDOUT "#!!# they precede any references to them.\n\n"; + print STDOUT "$macro_output\n"; + } + +# Output some default pointers to ACLs for RCPT and DATA time. If no Exim 3 +# options that apply are set, non-restricting ACLs are generated. + +print STDOUT "#!!# These options specify the Access Control Lists (ACLs) that\n"; +print STDOUT "#!!# are used for incoming SMTP messages - after the RCPT and DATA\n"; +print STDOUT "#!!# commands, respectively.\n\n"; + +print STDOUT "acl_smtp_rcpt = check_recipient\n"; +print STDOUT "acl_smtp_data = check_message\n\n"; + +if (defined $main{"auth_over_tls_hosts"}) + { + print STDOUT "#!!# This option specifies the Access Control List (ACL) that\n"; + print STDOUT "#!!# is used after an AUTH command.\n\n"; + print STDOUT "acl_smtp_auth = check_auth\n\n"; + } + +if (&bool("smtp_verify") || + defined $main{"smtp_etrn_hosts"} || + defined $main{"smtp_expn_hosts"}) + { + print STDOUT "#!!# These options specify the Access Control Lists (ACLs) that\n"; + print STDOUT "#!!# are used to control the ETRN, EXPN, and VRFY commands.\n"; + print STDOUT "#!!# Where no ACL is defined, the command is locked out.\n\n"; + + print STDOUT "acl_smtp_etrn = check_etrn\n" if defined $main{"smtp_etrn_hosts"}; + print STDOUT "acl_smtp_expn = check_expn\n" if defined $main{"smtp_expn_hosts"}; + print STDOUT "acl_smtp_vrfy = check_vrfy\n" if &bool("smtp_verify"); + print STDOUT "\n"; + } + +# If local_domains was set, get its value; otherwise set to "@". Add into it +# appropriate magic for local_domains_include_host[_literals]. + +$local_domains = (defined $main{"local_domains"})? $main{"local_domains"} : "@"; + +$ldsep = ":"; +if ($local_domains =~ /^\s*<(.)\s*(.*)/s) + { + $ldsep = $1; + $local_domains = $2; + } + +$local_domains = "\@[] $ldsep " . $local_domains + if defined $main{"local_domains_include_host_literals"} && + $main{"local_domains_include_host_literals"} eq "true"; + +$local_domains = "\@ $ldsep " . $local_domains + if defined $main{"local_domains_include_host"} && + $main{"local_domains_include_host"} eq "true"; + +$local_domains = "<$ldsep " . $local_domains if $ldsep ne ":"; + +# Output a domain list setting for these domains, provided something is defined + +if ($local_domains !~ /^\s*$/) + { + print STDOUT "#!!# This setting defines a named domain list called\n"; + print STDOUT "#!!# local_domains, created from the old options that\n"; + print STDOUT "#!!# referred to local domains. It will be referenced\n"; + print STDOUT "#!!# later on by the syntax \"+local_domains\".\n"; + print STDOUT "#!!# Other domain and host lists may follow.\n\n"; + + printf STDOUT ("domainlist local_domains = %s\n\n", + &no_expand_regex($local_domains)); + } + +$relay_domains = (defined $main{"relay_domains"})? $main{"relay_domains"} : ""; + +$ldsep = ":"; +if ($relay_domains =~ /^\s*<(.)\s*(.*)/s) + { + $ldsep = $1; + } + +if (defined $main{"relay_domains_include_local_mx"}) + { + $relay_domains .= ($relay_domains =~ /^\s*$/)? "\@mx_any" : + " $ldsep \@mx_any"; + } + +printf STDOUT ("domainlist relay_domains = %s\n", + &no_expand_regex($relay_domains)) + if $relay_domains !~ /^\s*$/; + + +# If ignore_errmsg_errors is set, we are going to force 0s as the value +# for ignore_errmsg_errors_after, so arrange to skip any other value. + +push @skipped_options, "ignore_errmsg_errors_after" + if &bool("ignore_errmsg_errors"); + + +# If rbl_domains is set, split it up and generate six lists: +# rbl_warn_domains, rbl_warn_domains_skiprelay +# rbl_reject_domains, rbl_reject_domains_skiprelay +# rbl_accept_domains, rbl_accept_domains_skiprelay + +if (defined $main{"rbl_domains"}) + { + my($s) = &unquote($main{"rbl_domains"}); + $s =~ s/\s*\\\s*\n\s*/ /g; + my(@list) = split /\s*:\s*/, $s; + + foreach $d (@list) + { + my(@sublist) = split /\//, $d; + my($name) = shift @sublist; + my($warn) = 0; + if (defined $main{"rbl_reject_recipients"}) + { + $warn = $main{"rbl_reject_recipients"} ne "true"; + } + + foreach $o (@sublist) + { + $warn = 1 if $o eq "warn"; + $warn = 0 if $o eq "reject"; + $warn = 2 if $o eq "accept"; + $skiprelay = 1 if $o eq "skiprelay"; + } + + if ($skiprelay) + { + if ($warn == 0) + { + $rbl_reject_skiprelay .= ((defined $rbl_reject_skiprelay)? ":":"").$name; + } + elsif ($warn == 1) + { + $rbl_warn_skiprelay .= ((defined $rbl_warn_skiprelay)? ":":"").$name; + } + elsif ($warn == 2) + { + $rbl_accept_skiprelay .= ((defined $rbl_accept_skiprelay)? ":":"").$name; + } + } + else + { + if ($warn == 0) + { + $rbl_reject_domains .= ((defined $rbl_reject_domains)? ":":"").$name; + } + elsif ($warn == 1) + { + $rbl_warn_domains .= ((defined $rbl_warn_domains)? ":":"").$name; + } + elsif ($warn == 2) + { + $rbl_accept_domains .= ((defined $rbl_accept_domains)? ":":"").$name; + } + } + } + } + + +# Output host list settings + +printf STDOUT ("hostlist auth_hosts = %s\n", + &no_expand_regex($main{"auth_hosts"})) + if defined $main{"auth_hosts"}; +printf STDOUT ("hostlist rbl_hosts = %s\n", + &no_expand_regex($main{"rbl_hosts"})) + if defined $main{"rbl_hosts"}; +printf STDOUT ("hostlist relay_hosts = %s\n", + &no_expand_regex($main{"host_accept_relay"})) + if defined $main{"host_accept_relay"}; +printf STDOUT ("hostlist auth_relay_hosts = %s\n", + &no_expand_regex($main{"host_auth_accept_relay"})) + if defined $main{"host_auth_accept_relay"}; + +printf STDOUT ("hostlist auth_over_tls_hosts = %s\n", + &no_expand_regex($main{"auth_over_tls_hosts"})) + if defined $main{"auth_over_tls_hosts"}; +printf STDOUT ("hostlist tls_hosts = %s\n", + &no_expand_regex($main{"tls_hosts"})) + if defined $main{"tls_hosts"}; +printf STDOUT ("hostlist tls_relay_hosts = %s\n", + &no_expand_regex($main{"tls_host_accept_relay"})) + if defined $main{"tls_host_accept_relay"}; + +print STDOUT "\n"; + + +# Convert various logging options + +$log_selector = ""; +$sep = " \\\n "; + +if (defined $main{"log_level"}) + { + my($level) = $main{"log_level"}; + $log_selector .= "$sep -retry_defer$sep -skip_delivery" if $level < 5; + $log_selector .= "$sep -lost_incoming_connection$sep -smtp_syntax_error" . + "$sep -delay_delivery" if $level < 4; + $log_selector .= "$sep -size_reject" if $level < 2; + } + +$log_selector .= "$sep -queue_run" + if defined $main{"log_queue_run_level"} && + defined $main{"log_level"} && + $main{"log_queue_run_level"} > $main{"log_level"}; + +$log_selector .= "$sep +address_rewrite" if &bool("log_rewrites"); +$log_selector .= "$sep +all_parents" if &bool("log_all_parents"); +$log_selector .= "$sep +arguments" if &bool("log_arguments"); +$log_selector .= "$sep +incoming_port" if &bool("log_incoming_port"); +$log_selector .= "$sep +incoming_interface" if &bool("log_interface"); +$log_selector .= "$sep +received_sender" if &bool("log_received_sender"); +$log_selector .= "$sep +received_recipients" if &bool("log_received_recipients"); +$log_selector .= "$sep +sender_on_delivery" if &bool("log_sender_on_delivery"); +$log_selector .= "$sep +smtp_confirmation" if &bool("log_smtp_confirmation"); +$log_selector .= "$sep +smtp_connection" if &bool("log_smtp_connections"); +$log_selector .= "$sep +smtp_syntax_error" if &bool("log_smtp_syntax_errors"); +$log_selector .= "$sep +subject" if &bool("log_subject"); +$log_selector .= "$sep +tls_cipher" if &bool("tls_log_cipher"); +$log_selector .= "$sep +tls_peerdn" if &bool("tls_log_peerdn"); + + +if ($log_selector ne "") + { + print STDOUT "#!!# All previous logging options are combined into a single\n" + . "#!!# option in Exim 4. This setting is an approximation to\n" + . "#!!# the previous state - some logging has changed.\n\n"; + print STDOUT "log_selector = $log_selector\n\n"; + } + +# If deliver_load_max is set, replace it with queue_only_load (taking the +# lower value if both set) and also set deliver_queue_load_max if it is +# not already set. When scanning for output, deliver_load_max is skipped. + +if (defined $main{"deliver_load_max"}) + { + &rubric(); + print STDERR "\n" . +"** deliver_load_max is abolished in Exim 4.\n"; + + if (defined $main{"queue_only_load"}) + { + $queue_only_load_was_present = 1; + if ($main{"queue_only_load"} < $main{"deliver_load_max"}) + { + print STDERR +" As queue_only_load was set lower, deliver_load_max is just removed.\n"; + } + else + { + print STDERR +" As queue_only_load was set higher, it's value has been replaced by\n" . +" the value of deliver_load_max.\n"; + $main{"queue_only_load"} = $main{"deliver_load_max"}; + } + } + else + { + print STDERR +" queue_only_load has been set to the load value.\n"; + $main{"queue_only_load"} = $main{"deliver_load_max"}; + } + + if (!defined $main{"deliver_queue_load_max"}) + { + print STDERR +" deliver_queue_load_max has been set to the value of queue_only_load.\n"; + $main{"deliver_queue_load_max"} = $main{"queue_only_load"}; + } + else + { + $deliver_queue_load_max_was_present = 1; + } + } + + +# Now we scan through the various parts of the file again, making changes +# as necessary. + +# -------- The main configuration -------- + +$prefix = ""; +MainLine: for ($i = 0; $i < $clen; $i++) + { + my($nextline) = ""; + $type = &checkline($c[$i]); + last if $type eq "end"; + + if ($type eq "macro") + { + $i++ while $c[$i] =~ /\\\s*$|^\s*#/; + next; + } + + if ($type eq "comment") { print STDOUT "$c[$i]\n"; next; } + + # Collect any continuation lines for an option setting + + while ($rest =~ /\\\s*$/s || $nextline =~ /^\s*#/) + { + $nextline = $c[++$i]; + $rest .= "\n$nextline"; + } + + $rest =~ s/^=\s*//; + + # Deal with main options that are skipped (they are used in other + # options in other places). + + for $skipped (@skipped_options) + { + next MainLine if $name eq $skipped; + } + + # Deal with main options that are totally abolished + + for $abolished (@abolished_options) + { + if ($name eq $abolished) + { + &rubric(); + print STDERR "\n" . +"** The $name option no longer exists, and has no equivalent\n" . +" in Exim 4.\n"; + next MainLine; + } + } + + # There is a special case for rbl_warn_header + + if ($name eq "rbl_warn_header") + { + &rubric(); + print STDERR "\n" . +"** The $name option no longer exists. In Exim 4 you can achieve the\n" . +" effect by adding a suitable \"message\" statement in the ACL.\n"; + } + + # There is a special case for sender_reject and host_reject + + elsif ($name eq "sender_reject" || $name eq "host_reject") + { + &rubric(); + print STDERR "\n" . +"** The $name option no longer exists. Its data has been used in\n" . +" an Access Control List as if it were in ${name}_recipients.\n"; + } + + # And a special message for prohibition_message + + elsif ($name eq "prohibition_message") + { + &rubric(); + print STDERR "\n" . +"** The prohibition_message option no longer exists. The facility is\n" . +" provided in a different way in Exim 4, via the \"message\" keyword\n" . +" in Access Control Lists. It isn't possible to do an automatic conversion,\n" . +" so the value of prohibition_message has been ignored. You will have to\n" . +" modify the ACLs if you want to reinstate the feature.\n"; + } + + # auth_always_advertise gets converted to auth_advertise_hosts + + elsif ($name eq "auth_always_advertise") + { + print STDOUT "#!!# auth_always_advertise converted to auth_advertise_hosts\n"; + if (&bool("auth_always_advertise")) + { + print STDOUT "auth_advertise_hosts = *\n"; + } + else + { + $sep = ""; + print STDOUT "auth_advertise_hosts ="; + if (defined $main{"auth_hosts"}) + { + print STDOUT "$sep +auth_hosts"; + $sep = " :"; + } + if (defined $main{"host_accept_relay"}) + { + print STDOUT "$sep !+relay_hosts"; + $sep = " :"; + } + if (defined $main{"host_auth_accept_relay"}) + { + print STDOUT "$sep +auth_relay_hosts"; + } + print STDOUT "\n"; + } + } + + # Deal with main options that have to be rewritten + + elsif ($name eq "accept_timeout") + { + print STDOUT "#!!# accept_timeout renamed receive_timeout\n"; + print STDOUT "receive_timeout = $rest\n"; + } + + elsif ($name eq "collapse_source_routes") + { + print STDOUT "#!!# collapse_source_routes removed\n"; + print STDOUT "#!!# It has been a no-op since 3.10.\n"; + } + + elsif ($name eq "daemon_smtp_service") + { + print STDOUT "#!!# daemon_smtp_service renamed daemon_smtp_port\n"; + print STDOUT "daemon_smtp_port = $rest\n"; + } + + elsif ($name eq "dns_check_names" || $name eq "dns_check_names_pattern") + { + if (!$done_dns_check_names) + { + if (&bool("dns_check_names")) + { + if (defined $main{"dns_check_names_pattern"}) + { + &outopt(\%main, "dns_check_names_pattern", 0); + } + } + + else + { + print STDOUT "#!!# dns_check_names has been abolished\n"; + print STDOUT "#!!# setting dns_check_pattern empty to turn off check\n"; + print STDOUT "dns_check_names_pattern =\n"; + } + + $done_dns_check_names = 1; + } + } + + elsif ($name eq "deliver_load_max") + { + print STDOUT "deliver_queue_load_max = $main{'deliver_queue_load_max'}\n" + if !$deliver_queue_load_max_was_present; + print STDOUT "queue_only_load = $main{'queue_only_load'}\n" + if !$queue_only_load_was_present; + } + + elsif ($name eq "errmsg_file") + { + print STDOUT "#!!# errmsg_file renamed bounce_message_file\n"; + print STDOUT "bounce_message_file = $rest\n"; + } + + elsif ($name eq "errmsg_text") + { + print STDOUT "#!!# errmsg_text renamed bounce_message_text\n"; + print STDOUT "bounce_message_text = $rest\n"; + } + + elsif ($name eq "forbid_domain_literals") + { + print STDOUT "#!!# forbid_domain_literals replaced by allow_domain_literals\n"; + print STDOUT "allow_domain_literals = ", + &bool("forbid_domain_literals")? "false" : "true", "\n"; + } + + elsif ($name eq "freeze_tell_mailmaster") + { + print STDOUT "#!!# freeze_tell_mailmaster replaced by freeze_tell\n"; + if (&bool("freeze_tell_mailmaster")) + { + print STDOUT "freeze_tell = ", + ((defined $main{"errors_address"})? + $main{"errors_address"} : "postmaster"), "\n"; + } + else + { + print STDOUT "#!!# freeze_tell is unset by default\n"; + } + } + + elsif ($name eq "helo_verify") + { + print STDOUT "#!!# helo_verify renamed helo_verify_hosts\n"; + printf STDOUT ("helo_verify_hosts = %s\n", &no_expand_regex($rest)); + } + + elsif ($name eq "ignore_errmsg_errors") + { + print STDOUT "ignore_bounce_errors_after = 0s\n"; + } + + elsif ($name eq "ignore_errmsg_errors_after") + { + print STDOUT "#!!# ignore_errmsg_errors_after renamed ignore_bounce_errors_after\n"; + print STDOUT "ignore_bounce_errors_after = $rest\n"; + } + + elsif ($name eq "ipv4_address_lookup" || $name eq "dns_ipv4_lookup") + { + print STDOUT "#!!# $name changed to dns_ipv4_lookup\n" + if $name eq "ipv4_address_lookup"; + print STDOUT "#!!# dns_ipv4_lookup is now a domain list\n"; + if (&bool($name)) + { + print STDOUT "dns_ipv4_lookup = *\n"; + } + else + { + print STDOUT "#!!# default for dns_ipv4_lookup is unset\n"; + } + } + + elsif ($name eq "locally_caseless") + { + print STDOUT "#!!# locally_caseless removed\n"; + print STDOUT "#!!# caseful_local_part will be added to ex-directors\n"; + $add_caseful_local_part = 1; + } + + elsif ($name eq "message_filter_directory2_transport") + { + print STDOUT "#!!# message_filter_directory2_transport removed\n"; + } + + elsif ($name =~ /^message_filter(.*)/) + { + print STDOUT "#!!# $name renamed system_filter$1\n"; + print STDOUT "system_filter$1 = $rest\n"; + } + + elsif ($name eq "queue_remote_domains") + { + print STDOUT "#!!# queue_remote_domains renamed queue_domains\n"; + printf STDOUT ("queue_domains = %s\n", &no_expand_regex($rest)); + } + + elsif ($name eq "receiver_unqualified_hosts") + { + print STDOUT "#!!# receiver_unqualified_hosts renamed recipient_unqualified_hosts\n"; + printf STDOUT ("recipient_unqualified_hosts = %s\n", + &no_expand_regex($rest)); + } + + elsif ($name eq "remote_sort") + { + print STDOUT "#!!# remote_sort renamed remote_sort_domains\n"; + printf STDOUT ("remote_sort_domains = %s\n", &no_expand_regex($rest)); + } + + elsif ($name eq "security") + { + if ($rest eq "unprivileged") + { + print STDOUT "#!!# security=unprivileged changed to deliver_drop_privilege\n"; + print STDOUT "deliver_drop_privilege\n"; + } + else + { + &rubric(); + print STDERR "\n" . +"** The 'security' option no longer exists.\n"; + } + } + + elsif ($name eq "timestamps_utc") + { + print STDOUT "#!!# timestamps_utc changed to use timezone\n"; + print STDOUT "timezone = utc\n"; + } + + elsif ($name eq "untrusted_set_sender") + { + print STDOUT "#!!# untrusted_set_sender is now a list of what can be set\n"; + print STDOUT "#!!# The default is an empty list.\n"; + if (&bool("untrusted_set_sender")) + { + print STDOUT "untrusted_set_sender = *\n"; + } + } + + elsif ($name eq "warnmsg_file") + { + print STDOUT "#!!# warnmsg_file renamed warn_message_file\n"; + print STDOUT "warn_message_file = $rest\n"; + } + + # Remaining options just get copied unless they are one of those that's + # a list where any regular expressions have to be escaped. + + else + { + my($no_expand) = 0; + foreach $o (@list_options) + { + if ($name eq $o) + { + $no_expand = 1; + last; + } + } + &outopt(\%main, $name, $no_expand); + } + } + + +# -------- The ACL configuration -------- + +print STDOUT "\n"; +print STDOUT "#!!#######################################################!!#\n"; +print STDOUT "#!!# This new section of the configuration contains ACLs #!!#\n"; +print STDOUT "#!!# (Access Control Lists) derived from the Exim 3 #!!#\n"; +print STDOUT "#!!# policy control options. #!!#\n"; +print STDOUT "#!!#######################################################!!#\n"; + +print STDOUT "\n"; +print STDOUT "#!!# These ACLs are crudely constructed from Exim 3 options.\n"; +print STDOUT "#!!# They are almost certainly not optimal. You should study\n"; +print STDOUT "#!!# them and rewrite as necessary.\n"; + +print STDOUT "\nbegin acl\n\n"; + + +# Output an ACL for use after the RCPT command. This combines all the previous +# policy checking options. + +print STDOUT "#!!# ACL that is used after the RCPT command\n"; +print STDOUT "check_recipient:\n"; + +print STDOUT " # Exim 3 had no checking on -bs messages, so for compatibility\n"; +print STDOUT " # we accept if the source is local SMTP (i.e. not over TCP/IP).\n"; +print STDOUT " # We do this by testing for an empty sending host field.\n"; +print STDOUT " accept hosts = :\n"; + +if (defined $main{"tls_verify_ciphers"}) + { + print STDOUT " deny "; + print STDOUT "hosts = $main{'tls_verify_hosts'}\n " + if defined $main{"tls_verify_hosts"}; + print STDOUT " encrypted = *\n "; + print STDOUT "!encrypted = $main{'tls_verify_ciphers'}\n"; + } + +print STDOUT " deny hosts = +auth_hosts\n" . + " message = authentication required\n" . + " !authenticated = *\n" + if defined $main{"auth_hosts"}; + +print STDOUT " deny hosts = +tls_hosts\n" . + " message = encryption required\n" . + " !encrypted = *\n" + if defined $main{"tls_hosts"}; + +printf STDOUT (" accept recipients = %s\n", + &acl_quote(&sort_address_list($main{"recipients_reject_except"}, + "recipients_reject_except"))) + if defined $main{"recipients_reject_except"}; + +printf STDOUT (" accept senders = %s\n", + &acl_quote(&sort_address_list($main{"recipients_reject_except_senders"}, + "recipients_reject_except_senders"))) + if defined $main{"recipients_reject_except_senders"}; + +printf STDOUT (" deny hosts = %s\n", &acl_quote($main{"host_reject"})) + if defined $main{"host_reject"}; + +printf STDOUT (" deny hosts = %s\n", + &acl_quote($main{"host_reject_recipients"})) + if defined $main{"host_reject_recipients"}; + +if (defined $main{"rbl_domains"}) + { + my($msg) = "message = host is listed in \$dnslist_domain\n "; + my($hlist) = (defined $main{"rbl_hosts"})? + "hosts = +rbl_hosts\n " : ""; + + print STDOUT " accept ${hlist}dnslists = $rbl_accept_domains\n" + if defined $rbl_accept_domains; + print STDOUT " deny ${hlist}${msg}dnslists = $rbl_reject_domains\n" + if defined $rbl_reject_domains; + print STDOUT " warn ${hlist}" . + "message = X-Warning: \$sender_host_address is listed at \$dnslist_domain\n" . + " dnslists = $rbl_warn_domains\n" + if defined $rbl_warn_domains; + + if (defined $main{"host_accept_relay"}) + { + $hlist .= "hosts = !+relay_hosts\n "; + print STDOUT " accept ${hlist}dnslists = $rbl_accept_skiprelay\n" + if defined $rbl_accept_skiprelay; + print STDOUT " deny ${hlist}${msg}dnslists = $rbl_reject_skiprelay\n" + if defined $rbl_reject_skiprelay; + print STDOUT " warn ${hlist}" . + "message = X-Warning: \$sender_host_address is listed at \$dnslist_domain\n" . + " dnslists = $rbl_warn_skiprelay\n" + if defined $rbl_warn_skiprelay; + } + } + +printf STDOUT (" deny senders = %s\n", + &acl_quote(&sort_address_list($main{"sender_reject"}, "sender_reject"))) + if defined $main{"sender_reject"}; + +printf STDOUT (" deny senders = %s\n", + &acl_quote(&sort_address_list($main{"sender_reject_recipients"}, + "sender_reject_recipients"))) + if defined $main{"sender_reject_recipients"}; + +if (&bool("sender_verify")) + { + if (defined $main{"sender_verify_hosts_callback"} && + defined $main{"sender_verify_callback_domains"}) + { + printf STDOUT (" deny hosts = %s\n", + &acl_quote($main{"sender_verify_hosts_callback"})); + printf STDOUT (" sender_domains = %s\n", + &acl_quote($main{"sender_verify_callback_domains"})); + print STDOUT " !verify = sender/callout"; + print STDOUT "=$main{\"sender_verify_callback_timeout\"}" + if defined $main{"sender_verify_callback_timeout"}; + print STDOUT "\n"; + } + + if (defined $main{"sender_verify_hosts"}) + { + printf STDOUT (" deny hosts = %s\n", + &acl_quote($main{"sender_verify_hosts"})); + print STDOUT " !verify = sender\n"; + } + else + { + print STDOUT " require verify = sender\n"; + } + } + +if (&bool("receiver_verify")) + { + print STDOUT " deny message = unrouteable address\n"; + printf STDOUT (" recipients = %s\n", + &acl_quote(&sort_address_list($main{"receiver_verify_addresses"}, + "receiver_verify_addresses"))) + if defined $main{"receiver_verify_addresses"}; + printf STDOUT (" hosts = %s\n", + &acl_quote($main{"receiver_verify_hosts"})) + if defined $main{"receiver_verify_hosts"}; + printf STDOUT (" senders = %s\n", + &acl_quote(&sort_address_list($main{"receiver_verify_senders"}, + "receiver_verify_senders"))) + if defined $main{"receiver_verify_senders"}; + print STDOUT " !verify = recipient\n"; + } + +print STDOUT " accept domains = +local_domains\n" + if $local_domains !~ /^\s*$/; + +print STDOUT " accept domains = +relay_domains\n" + if $relay_domains !~ /^\s*$/; + +if (defined $main{"host_accept_relay"}) + { + if (defined $main{"sender_address_relay"}) + { + if (defined $main{"sender_address_relay_hosts"}) + { + printf STDOUT (" accept hosts = %s\n", + &acl_quote($main{"sender_address_relay_hosts"})); + print STDOUT " endpass\n"; + print STDOUT " message = invalid sender\n"; + printf STDOUT (" senders = %s\n", + &acl_quote(&sort_address_list($main{"sender_address_relay"}, + "sender_address_relay"))); + print STDOUT " accept hosts = +relay_hosts\n"; + } + else + { + print STDOUT " accept hosts = +relay_hosts\n"; + print STDOUT " endpass\n"; + print STDOUT " message = invalid sender\n"; + printf STDOUT (" senders = %s\n", + &acl_quote(&sort_address_list($main{"sender_address_relay"}, + "sender_address_relay"))); + } + } + else + { + print STDOUT " accept hosts = +relay_hosts\n"; + } + } + +print STDOUT " accept hosts = +auth_relay_hosts\n" . + " endpass\n" . + " message = authentication required\n" . + " authenticated = *\n" + if defined $main{"host_auth_accept_relay"}; + +print STDOUT " accept hosts = +tls_relay_hosts\n" . + " endpass\n" . + " message = encryption required\n" . + " encrypted = *\n" + if defined $main{"tls_host_accept_relay"}; + +print STDOUT " deny message = relay not permitted\n\n"; + + +# Output an ACL for use after the DATA command. This is concerned with +# header checking. + +print STDOUT "#!!# ACL that is used after the DATA command\n"; +print STDOUT "check_message:\n"; + +# Default for headers_checks_fail is true + +if (!defined $main{"headers_checks_fail"} || + $main{"headers_checks_fail"} eq "true") + { + print STDOUT " require verify = header_syntax\n" + if &bool("headers_check_syntax"); + print STDOUT " require verify = header_sender\n" + if &bool("headers_sender_verify"); + print STDOUT " accept senders = !:\n require verify = header_sender\n" + if &bool("headers_sender_verify_errmsg"); + } +else + { + print STDOUT " warn !verify = header_syntax\n" + if &bool("headers_check_syntax"); + print STDOUT " warn !verify = header_sender\n" + if &bool("headers_sender_verify"); + print STDOUT " accept senders = !:\n warn !verify = header_sender\n" + if &bool("headers_sender_verify_errmsg"); + } + +print STDOUT " accept\n\n"; + + +# Output an ACL for AUTH if required + +if (defined $main{"auth_over_tls_hosts"}) + { + print STDOUT "#!!# ACL that is used after the AUTH command\n" . + "check_auth:\n" . + " accept hosts = +auth_over_tls_hosts\n" . + " endpass\n" . + " message = STARTTLS required before AUTH\n" . + " encrypted = *\n" . + " accept\n"; + } + + +# Output ACLs for ETRN, EXPN, and VRFY if required + +if (defined $main{"smtp_etrn_hosts"}) + { + print STDOUT "#!!# ACL that is used after the ETRN command\n" . + "check_etrn:\n"; + print STDOUT " deny hosts = +auth_hosts\n" . + " message = authentication required\n" . + " !authenticated = *\n" + if defined $main{"auth_hosts"}; + print STDOUT " accept hosts = $main{\"smtp_etrn_hosts\"}\n\n"; + } + +if (defined $main{"smtp_expn_hosts"}) + { + print STDOUT "#!!# ACL that is used after the EXPN command\n" . + "check_expn:\n"; + print STDOUT " deny hosts = +auth_hosts\n" . + " message = authentication required\n" . + " !authenticated = *\n" + if defined $main{"auth_hosts"}; + print STDOUT " accept hosts = $main{\"smtp_expn_hosts\"}\n\n"; + } + +if (&bool("smtp_verify")) + { + print STDOUT "#!!# ACL that is used after the VRFY command\n" . + "check_vrfy:\n"; + print STDOUT " deny hosts = +auth_hosts\n" . + " message = authentication required\n" . + " !authenticated = *\n" + if defined $main{"auth_hosts"}; + print STDOUT " accept\n\n"; + } + +# -------- The authenticators -------- + +$started = 0; +for ($i = $auth_start; $i < $clen; $i++) + { + if (!$started) + { + if ($c[$i] !~ /^\s*(#|$)/) + { + print STDOUT "\nbegin authenticators\n\n"; + $started = 1; + } + } + print STDOUT "$c[$i]\n"; + } + + +# -------- Rewrite section -------- + +$started = 0; +for ($i = $rewrite_start; $i < $clen && $i < $auth_start - 1; $i++) + { + if (!$started) + { + if ($c[$i] !~ /^\s*(#|$)/) + { + print STDOUT "\nbegin rewrite\n\n"; + $started = 1; + } + } + &print_no_expand($c[$i]); + } + + +# -------- The routers configuration -------- + +# The new routers configuration is created out of the old directors and routers +# configuration. We put the old routers first, adding a "domains" option to +# any that don't have one, to make them select the domains that do not match +# the original local_domains. The routers get modified as necessary, and the +# final one has "no_more" set, unless it has conditions. In that case we have +# to add an extra router to be sure of failing all non-local addresses that +# fall through. We do this also if there are no routers at all. The old +# directors follow, modified as required. + +$prefix = "r."; +undef @comments; + +print STDOUT "\n"; +print STDOUT "#!!#######################################################!!#\n"; +print STDOUT "#!!# Here follow routers created from the old routers, #!!#\n"; +print STDOUT "#!!# for handling non-local domains. #!!#\n"; +print STDOUT "#!!#######################################################!!#\n"; + +print STDOUT "\nbegin routers\n\n"; + +for ($i = $router_start; $i < $clen; $i++) + { + $type = &checkline($c[$i]); + last if $type eq "end"; + + if ($type eq "comment") { push(@comments, "$c[$i]\n"); next; } + + # When we hit the start of a driver, modify its options as necessary, + # and then output it from the stored option settings, having first output + # and previous comments. + + if ($type eq "driver") + { + print STDOUT shift @comments while scalar(@comments) > 0; + + $hash = $driverlist{"$prefix$name"}; + $driver = $$hash{"driver"}; + print STDOUT "$name:\n"; + + $add_no_more = + ! defined $$hash{"domains"} && + ! defined $$hash{"local_parts"} && + ! defined $$hash{"senders"} && + ! defined $$hash{"condition"} && + ! defined $$hash{"require_files"} && + (!defined $$hash{"verify_only"} || $$hash{"verify_only"} eq "false") && + (!defined $$hash{"verify"} || $$hash{"verify"} eq "true"); + + # Create a "domains" setting if there isn't one, unless local domains + # was explicitly empty. + + $$hash{"domains"} = "! +local_domains" + if !defined $$hash{"domains"} && $local_domains !~ /^\s*$/; + + # If the router had a local_parts setting, add caseful_local_part + + $$hash{"caseful_local_part"} = "true" if defined $$hash{"local_parts"}; + + # If the router has "self=local" set, change it to "self=pass", and + # set pass_router to the router that was the first director. Change the + # obsolete self settings of "fail_hard" and "fail_soft" to "fail" and + # "pass". + + if (defined $$hash{"self"}) + { + if ($$hash{"self"} eq "local") + { + $$hash{"self"} = "pass"; + $$hash{"pass_router"} = $first_director; + } + elsif ($$hash{"self"} eq "fail_hard") + { + $$hash{"self"} = "fail"; + } + elsif ($$hash{"self"} eq "fail_soft") + { + $$hash{"self"} = "pass"; + } + } + + # If the router had a require_files setting, check it for user names + # and colons that are part of expansion items + + if (defined $$hash{"require_files"}) + { + &check_require($$hash{"require_files"}, "'$name' router"); + if (($$hash{"require_files"} =~ s/(\$\{\w+):/$1::/g) > 0 || + ($$hash{"require_files"} =~ s/ldap:/ldap::/g) > 0) + { + &rubric(); + print STDERR "\n" . +"*** A setting of require_files in the $name router contains\n" . +" a colon in what appears to be an expansion item. In Exim 3, the\n" . +" whole string was expanded before splitting the list, but in Exim 4\n" . +" each item is expanded separately, so colons that are not list\n" . +" item separators have to be doubled. One or more such colons in this\n" . +" list have been doubled as a precaution. Please check the result.\n"; + } + } + + # If the router had a "senders" setting, munge the address list + + $$hash{"senders"} = &sort_address_list($$hash{"senders"}, "senders") + if defined $$hash{"senders"}; + + # ---- Changes to domainlist router ---- + + if ($driver eq "domainlist") + { + &abolished($hash, "A domainlist router", + "modemask", "owners", "owngroups", + "qualify_single", "search_parents"); + + # The name has changed + + $$hash{"driver"} = "manualroute"; + + # Turn "route_file", "route_query" and "route_queries" into lookups for + # route_data. + + if (defined $$hash{"route_file"}) + { + $$hash{"route_data"} = "\${lookup\{\$domain\}$$hash{'search_type'}" . + "\{$$hash{'route_file'}\}\}"; + } + elsif (defined $$hash{"route_query"}) + { + $$hash{"route_data"} = "\${lookup $$hash{'search_type'}" . + "\{" . &unquote($$hash{'route_query'}) . "\}\}"; + } + elsif (defined $$hash{"route_queries"}) + { + $endkets = 0; + $$hash{"route_data"} = ""; + $route_queries = $$hash{'route_queries'}; + $route_queries =~ s/^"(.*)"$/$1/s; + $route_queries =~ s/::/++colons++/g; + @qq = split(/:/, $route_queries); + + foreach $q (@qq) + { + $q =~ s/\+\+colons\+\+/:/g; + $q =~ s/^\s+//; + $q =~ s/\s+$//; + if ($endkets > 0) + { + $$hash{"route_data"} .= "\\\n {"; + $endkets++; + } + $$hash{"route_data"} .= "\${lookup $$hash{'search_type'} \{$q\}\{\$value\}"; + $endkets++; + } + + $$hash{"route_data"} .= "}" x $endkets; + } + + delete $$hash{"route_file"}; + delete $$hash{"route_query"}; + delete $$hash{"route_queries"}; + delete $$hash{"search_type"}; + + # But we can't allow both route_data and route_list + + if (defined $$hash{"route_data"} && defined $$hash{"route_list"}) + { + &rubric(); + print STDERR "\n" . +"** An Exim 3 'domainlist' router called '$name' contained a 'route_list'\n" . +" option as well as a setting of 'route_file', 'route_query', or\n" . +" 'route_queries'. The latter has been turned into a 'route_data' setting,\n". +" but in Exim 4 you can't have both 'route_data' and 'route_list'. You'll\n" . +" have to rewrite this router; in the meantime, 'route_list' has been\n" . +" omitted.\n"; + print STDOUT "#!!# route_list option removed\n"; + delete $$hash{"route_list"}; + } + + # Change bydns_a into bydns in a route_list; also bydns_mx, but that + # works differently. + + if (defined $$hash{"route_list"}) + { + $$hash{"route_list"} =~ s/bydns_a/bydns/g; + if ($$hash{"route_list"} =~ /bydns_mx/) + { + $$hash{"route_list"} =~ s/bydns_mx/bydns/g; + &rubric(); + print STDERR "\n" . +"*** An Exim 3 'domainlist' router called '$name' contained a 'route_list'\n" . +" option which used 'bydns_mx'. This feature no longer exists in Exim 4.\n" . +" It has been changed to 'bydns', but it won't have the same effect,\n" . +" because it will look for A rather than MX records. Use the 'dnslookup'\n" . +" router to do MX lookups - if you want to override the hosts found from\n" . +" MX records, you should route to a special 'smtp' transport which has\n" . +" both 'hosts' and 'hosts_override' set.\n"; + } + } + + # Arrange to not expand regex + + $$hash{"route_list"} = &no_expand_regex($$hash{"route_list"}, ";") + if (defined $$hash{"route_list"}) + } + + + # ---- Changes to iplookup router ---- + + elsif ($driver eq "iplookup") + { + &renamed($hash, "service", "port"); + } + + + # ---- Changes to lookuphost router ---- + + elsif ($driver eq "lookuphost") + { + $$hash{"driver"} = "dnslookup"; + + if (defined $$hash{"gethostbyname"}) + { + &rubric(); + print STDERR "\n" . +"** An Exim 3 'lookuphost' router called '$name' used the 'gethostbyname'\n" . +" option, which no longer exists. You will have to rewrite it.\n"; + print STDOUT "#!!# gethostbyname option removed\n"; + delete $$hash{"gethostbyname"}; + } + + $$hash{"mx_domains"} = &no_expand_regex($$hash{"mx_domains"}) + if defined $$hash{"mx_domains"}; + } + + + # ---- Changes to the queryprogram router ---- + + elsif ($driver eq "queryprogram") + { + &rubric(); + print STDERR "\n" . +"** The configuration contains a 'queryprogram' router. Please note that\n" . +" the specification for the text that is returned by the program run\n" . +" by this router has changed in Exim 4. You will need to modify your\n" . +" program.\n"; + + if (!defined $$hash{'command_user'}) + { + &rubric(); + print STDERR "\n" . +"** The 'queryprogram' router called '$name' does not have a setting for\n" . +" the 'command_user' option. This is mandatory in Exim 4. A setting of\n" . +" 'nobody' has been created.\n"; + $$hash{"command_user"} = "nobody"; + } + } + + + # ------------------------------------- + + # Output the router's option settings + + &outdriver($hash); + next; + } + + # Skip past any continuation lines for an option setting + while ($c[$i] =~ /\\\s*$/s && $i < $clen - 1) + { + $i++; + $i++ while ($c[$i] =~ /^\s*#/); + } + } + +# Add "no_more" to the final driver from the old routers, provided it had no +# conditions. Otherwise, or if there were no routers, make up one to fail all +# non-local domains. + +if ($add_no_more) + { + print STDOUT " no_more\n"; + print STDOUT shift @comments while scalar(@comments) > 0; + } +else + { + print STDOUT shift @comments while scalar(@comments) > 0; + print STDOUT "\n#!!# This new router is put here to fail all domains that\n"; + print STDOUT "#!!# were not in local_domains in the Exim 3 configuration.\n\n"; + print STDOUT "fail_remote_domains:\n"; + print STDOUT " driver = redirect\n"; + print STDOUT " domains = ! +local_domains\n"; + print STDOUT " allow_fail\n"; + print STDOUT " data = :fail: unrouteable mail domain \"\$domain\"\n\n"; + } + +# Now copy the directors, making appropriate changes + +print STDOUT "\n"; +print STDOUT "#!!#######################################################!!#\n"; +print STDOUT "#!!# Here follow routers created from the old directors, #!!#\n"; +print STDOUT "#!!# for handling local domains. #!!#\n"; +print STDOUT "#!!#######################################################!!#\n"; + +$prefix = "d."; +for ($i = $director_start; $i < $clen; $i++) + { + $type = &checkline($c[$i]); + last if $type eq "end"; + + if ($type eq "comment") { print STDOUT "$c[$i]\n"; next; } + + undef $second_router; + + if ($type eq "driver") + { + $hash = $driverlist{"$prefix$name"}; + $driver = $$hash{"driver"}; + print STDOUT "$name:\n"; + + $$hash{"caseful_local_part"} = "true" if $add_caseful_local_part; + + if (defined $$hash{"local_parts"} && + (defined $$hash{"prefix"} || defined $hash{"suffix"})) + { + &rubric(); + print STDERR "\n" . +"** The Exim 3 configuration contains a director called '$name' which has\n" . +" 'local_parts' set, together with either or both of 'prefix' and 'suffix'\n". +" This combination has a different effect in Exim 4, where the affix\n" . +" is removed *before* 'local_parts' is tested. You will probably need\n" . +" to make changes to this driver.\n"; + } + + &renamed($hash, "prefix", "local_part_prefix"); + &renamed($hash, "prefix_optional", "local_part_prefix_optional"); + &renamed($hash, "suffix", "local_part_suffix"); + &renamed($hash, "suffix_optional", "local_part_suffix_optional"); + &renamed($hash, "new_director", "redirect_router"); + + &handle_current_and_home_directory($hash, $driver, $name); + + # If the director had a require_files setting, check it for user names + # and colons that are part of expansion items + + if (defined $$hash{"require_files"}) + { + &check_require($$hash{"require_files"}, "'$name' director"); + if (($$hash{"require_files"} =~ s/(\$\{\w+):/$1::/g) > 0 || + ($$hash{"require_files"} =~ s/ldap:/ldap::/g) > 0) + { + &rubric(); + print STDERR "\n" . +"*** A setting of require_files in the $name director contains\n" . +" a colon in what appears to be an expansion item. In Exim 3, the\n" . +" whole string was expanded before splitting the list, but in Exim 4\n" . +" each item is expanded separately, so colons that are not list\n" . +" item separators have to be doubled. One or more such colons in this\n" . +" list have been doubled as a precaution. Please check the result.\n"; + } + } + + # If the director had a "senders" setting, munge the address list + + $$hash{"senders"} = &sort_address_list($$hash{"senders"}, "senders") + if defined $$hash{"senders"}; + + # ---- Changes to aliasfile director ---- + + if ($driver eq "aliasfile") + { + &abolished($hash, "An aliasfile director", + "directory2_transport", "freeze_missing_include", + "modemask", "owners", "owngroups"); + + $$hash{"driver"} = "redirect"; + + $key = "\$local_part"; + $key = "\$local_part\@\$domain" + if defined $$hash{"include_domain"} && + $$hash{"include_domain"} eq "true"; + delete $$hash{"include_domain"}; + + if (defined $$hash{"forbid_special"} && $$hash{"forbid_special"} eq "true") + { + $$hash{"forbid_blackhole"} = "true"; + } + else + { + $$hash{"allow_defer"} = "true"; + $$hash{"allow_fail"} = "true"; + } + delete $$hash{"forbid_special"}; + + # Deal with "file", "query", or "queries" + + if (defined $$hash{"file"}) + { + $$hash{"data"} = + "\$\{lookup\{$key\}$$hash{'search_type'}\{$$hash{'file'}\}\}"; + if (defined $$hash{"optional"} && $$hash{"optional"} eq "true") + { + $$hash{"data"} = + "\$\{if exists\{$$hash{'file'}\}\{$$hash{'data'}\}\}"; + } + delete $$hash{"optional"}; + } + elsif (defined $$hash{"query"}) + { + &abolished($hash, "An aliasfile director", "optional"); + $$hash{"data"} = "\${lookup $$hash{'search_type'} " . + "\{" . &unquote($$hash{'query'}) . "\}\}"; + } + else # Must be queries + { + &abolished($hash, "An aliasfile director", "optional"); + $endkets = 0; + $$hash{"data"} = ""; + $queries = $$hash{'queries'}; + $queries =~ s/^"(.*)"$/$1/s; + $queries =~ s/::/++colons++/g; + @qq = split(/:/, $queries); + + foreach $q (@qq) + { + $q =~ s/\+\+colons\+\+/:/g; + $q =~ s/^\s+//; + $q =~ s/\s+$//; + if ($endkets > 0) + { + $$hash{"data"} .= "\\\n {"; + $endkets++; + } + $$hash{"data"} .= "\${lookup $$hash{'search_type'} \{$q\}\{\$value\}"; + $endkets++; + } + + $$hash{"data"} .= "}" x $endkets; + } + + $$hash{"data"} = "\${expand:$$hash{'data'}\}" + if (defined $$hash{"expand"} && $$hash{"expand"} eq "true"); + + delete $$hash{"expand"}; + delete $$hash{"file"}; + delete $$hash{"query"}; + delete $$hash{"queries"}; + delete $$hash{"search_type"}; + + # Turn aliasfile + transport into accept + condition + + if (defined $$hash{'transport'}) + { + &rubric(); + if (!defined $$hash{'condition'}) + { + print STDERR "\n" . +"** The Exim 3 configuration contains an aliasfile director called '$name',\n". +" which has 'transport' set. This has been turned into an 'accept' router\n". +" with a 'condition' setting, but should be carefully checked.\n"; + $$hash{'driver'} = "accept"; + $$hash{'condition'} = + "\$\{if eq \{\}\{$$hash{'data'}\}\{no\}\{yes\}\}"; + delete $$hash{'data'}; + delete $$hash{'allow_defer'}; + delete $$hash{'allow_fail'}; + } + else + { + print STDERR "\n" . +"** The Exim 3 configuration contains an aliasfile director called '$name',\n". +" which has 'transport' set. This cannot be turned into an 'accept' router\n". +" with a 'condition' setting, because there is already a 'condition'\n" . +" setting. It has been left as 'redirect' with a transport, which is\n" . +" invalid - you must sort this one out.\n"; + } + } + } + + + # ---- Changes to forwardfile director ---- + + elsif ($driver eq "forwardfile") + { + &abolished($hash, "A forwardfile director", + "check_group", "directory2_transport", + "freeze_missing_include", "match_directory", + "seteuid"); + + &renamed($hash, "filter", "allow_filter"); + + $$hash{"driver"} = "redirect"; + $$hash{"check_local_user"} = "true" + if !defined $$hash{"check_local_user"}; + + if (defined $$hash{"forbid_pipe"} && $$hash{"forbid_pipe"} eq "true") + { + print STDOUT "#!!# forbid_filter_run added because forbid_pipe is set\n"; + $$hash{"forbid_filter_run"} = "true"; + } + + if (defined $$hash{'allow_system_actions'} && + $$hash{'allow_system_actions'} eq 'true') + { + $$hash{'allow_freeze'} = "true"; + } + delete $$hash{'allow_system_actions'}; + + # If file_directory is defined, use it to qualify relative paths; if not, + # and check_local_user is defined, use $home. Remove file_directory from + # the output. + + $dir = ""; + if (defined $$hash{"file_directory"}) + { + $dir = $$hash{"file_directory"} . "/"; + delete $$hash{"file_directory"}; + } + elsif ($$hash{"check_local_user"} eq "true") + { + $dir = "\$home/"; + } + + # If it begins with an upper case letter, guess that this is really + # a macro. + + if (defined $$hash{"file"} && $$hash{"file"} !~ /^[\/A-Z]/) + { + $$hash{"file"} = $dir . $$hash{"file"}; + } + } + + + # ---- Changes to localuser director ---- + + elsif ($driver eq "localuser") + { + &abolished($hash, "A localuser director", "match_directory"); + $$hash{"driver"} = "accept"; + $$hash{"check_local_user"} = "true"; + } + + + # ---- Changes to smartuser director ---- + + elsif ($driver eq "smartuser") + { + &abolished($hash, "A smartuser director", "panic_expansion_fail"); + + $transport = $$hash{"transport"}; + $new_address = $$hash{"new_address"}; + + if (defined $transport && defined $new_address) + { + &rubric(); + print STDERR "\n" . +"** The Exim 3 configuration contains a smartuser director called '$name',\n". +" which has both 'transport' and 'new_address' set. This has been turned\n". +" into two routers for Exim 4. However, if the new address contains a\n" . +" reference to \$local_part, this won't work correctly. In any case, you\n". +" may be able to make it tidier by rewriting.\n"; + $$hash{"driver"} = "redirect"; + $$hash{"data"} = $new_address; + $$hash{"redirect_router"} = "${name}_part2"; + + $second_router = "\n". + "#!!# This router is invented to go with the previous one because\n". + "#!!# in Exim 4 you can't have a change of address and a transport\n". + "#!!# setting in the same router as you could in Exim 3.\n\n" . + "${name}_part2:\n". + " driver = accept\n". + " condition = \$\{if eq\{\$local_part@\$domain\}" . + "\{$new_address\}\{yes\}\{no\}\}\n". + " transport = $$hash{'transport'}\n"; + + delete $$hash{"new_address"}; + delete $$hash{"transport"}; + } + elsif (defined $new_address) + { + $$hash{"driver"} = "redirect"; + $$hash{"data"} = $new_address; + $$hash{"allow_defer"} = "true"; + $$hash{"allow_fail"} = "true"; + delete $$hash{"new_address"}; + } + else # Includes the case of neither set (verify_only) + { + $$hash{"driver"} = "accept"; + if (defined $$hash{"rewrite"}) + { + &rubric(); + print STDERR "\n" . +"** The Exim 3 configuration contains a setting of the 'rewrite' option on\n". +" a smartuser director called '$name', but this director does not have\n". +" a setting of 'new_address', so 'rewrite' has no effect. The director\n". +" has been turned into an 'accept' router, and 'rewrite' has been discarded."; + delete $$hash{"rewrite"}; + } + } + } + + + # ------------------------------------- + + # For ex-directors that don't have check_local_user set, add + # retry_use_local_part to imitate what Exim 3 would have done. + + $$hash{"retry_use_local_part"} = "true" + if (!defined $$hash{"check_local_user"} || + $$hash{"check_local_user"} eq "false") ; + + # Output the router's option settings + + &outdriver($hash); + + # Output an auxiliary router if one is needed + + print STDOUT $second_router if defined $second_router; + + next; + } + + # Skip past any continuation lines for an option setting + while ($c[$i] =~ /\\\s*$/s) + { + $i++; + $i++ while ($c[$i] =~ /^\s*#/); + } + } + + + +# -------- The transports configuration -------- + +$started = 0; +$prefix = "t."; +for ($i = $transport_start; $i < $clen; $i++) + { + $type = &checkline($c[$i]); + last if $type eq "end"; + + if ($type eq "comment") { print STDOUT "$c[$i]\n"; next; } + + if (!$started) + { + print STDOUT "begin transports\n\n"; + $started = 1; + } + + if ($type eq "driver") + { + $hash = $driverlist{"$prefix$name"}; + $driver = $$hash{"driver"}; + print STDOUT "$name:\n"; + + # ---- Changes to the appendfile transport ---- + + if ($driver eq "appendfile") + { + &renamed($hash, "prefix", "message_prefix"); + &renamed($hash, "suffix", "message_suffix"); + &abolished($hash, "An appendfile transport", + "require_lockfile"); + &handle_batch_and_bsmtp($hash); + if (defined $$hash{"from_hack"} && $$hash{"from_hack"} eq "false") + { + print STDOUT "#!!# no_from_hack replaced by check_string\n"; + $$hash{"check_string"} = ""; + } + delete $$hash{"from_hack"}; + } + + # ---- Changes to the lmtp transport ---- + + elsif ($driver eq "lmtp") + { + if (defined $$hash{"batch"} && $$hash{"batch"} ne "none") + { + $$hash{"batch_max"} = "100" if !defined $$hash{"batch_max"}; + $$hash{"batch_id"} = "\$domain" if $$hash{"batch"} eq "domain"; + } + else + { + $$hash{"batch_max"} = "1" if defined $$hash{"batch_max"}; + } + delete $$hash{"batch"}; + } + + # ---- Changes to the pipe transport ---- + + elsif ($driver eq "pipe") + { + &renamed($hash, "prefix", "message_prefix"); + &renamed($hash, "suffix", "message_suffix"); + &handle_batch_and_bsmtp($hash); + if (defined $$hash{"from_hack"} && $$hash{"from_hack"} eq "false") + { + print STDOUT "#!!# no_from_hack replaced by check_string\n"; + $$hash{"check_string"} = ""; + } + delete $$hash{"from_hack"}; + } + + # ---- Changes to the smtp transport ---- + + elsif ($driver eq "smtp") + { + &abolished($hash, "An smtp transport", "mx_domains"); + &renamed($hash, "service", "port"); + &renamed($hash, "tls_verify_ciphers", "tls_require_ciphers"); + &renamed($hash, "authenticate_hosts", "hosts_try_auth"); + + if (defined $$hash{"batch_max"}) + { + print STDOUT "#!!# batch_max renamed connection_max_messages\n"; + $$hash{"connection_max_messages"} = $$hash{"batch_max"}; + delete $$hash{"batch_max"}; + } + + foreach $o ("hosts_try_auth", "hosts_avoid_tls", "hosts_require_tls", + "mx_domains", "serialize_hosts") + { + $$hash{$o} = &no_expand_regex($$hash{$o}) if defined $$hash{$o}; + } + } + + &outdriver($driverlist{"$prefix$name"}); + next; + } + + # Skip past any continuation lines for an option setting + while ($c[$i] =~ /\\\s*$/s) + { + $i++; + $i++ while ($c[$i] =~ /^\s*#/); + } + } + + +# -------- The retry configuration -------- + +$started = 0; +for ($i = $retry_start; $i < $clen && $i < $rewrite_start - 1; $i++) + { + if (!$started) + { + if ($c[$i] !~ /^\s*(#|$)/) + { + print STDOUT "\nbegin retry\n\n"; + $started = 1; + } + } + &print_no_expand($c[$i]); + } + +print STDOUT "\n# End of Exim 4 configuration\n"; + +print STDERR "\n*******************************************************\n"; +print STDERR "***** Please review the generated file carefully. *****\n"; +print STDERR "*******************************************************\n\n"; + +# End of convert4r4 + diff --git a/src/crypt16.c b/src/crypt16.c new file mode 100644 index 0000000..56353c3 --- /dev/null +++ b/src/crypt16.c @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2000-2002 + * Chris Adams + * written for HiWAAY Internet Services + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +/* + * Adapted for Exim by Tamas TEVESZ + * Further adapted by Philip Hazel to cut out this function for operating + * systems that have a built-in version. + */ + +/* The OS has a built-in crypt16(). Some compilers don't like compiling empty +modules, so keep them happy with a dummy when skipping the rest. */ + +#include "config.h" + +#ifdef HAVE_CRYPT16 +static void dummy(int x) { dummy(x-1); } +#else + +/* The OS doesn't have a built-in crypt16(). Compile this one. */ + +#include +#include +#include "os.h" + +#ifdef CRYPT_H +#include +#endif + +char * +crypt16(char *key, char *salt) +{ +static char res[25]; /* Not threadsafe; like crypt() */ +static char s2[3]; +char *p; + +/* Clear the string of any previous data */ +memset (res, 0, sizeof (res)); + +/* crypt the first part */ +if (!(p = crypt (key, salt))) return NULL; +strncpy (res, p, 13); + +if (strlen (key) > 8) + { + /* crypt the rest + * the first two characters of the first block (not counting + * the salt) make up the new salt */ + + strncpy (s2, res+2, 2); + p = crypt (key+8, s2); + strncpy (res+13, p+2, 11); + memset (s2, 0, sizeof(s2)); + } + +return (res); +} +#endif + +/* End of crypt16.c */ diff --git a/src/daemon.c b/src/daemon.c new file mode 100644 index 0000000..8e8a515 --- /dev/null +++ b/src/daemon.c @@ -0,0 +1,2654 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Functions concerned with running Exim as a daemon */ + + +#include "exim.h" + + +/* Structure for holding data for each SMTP connection */ + +typedef struct smtp_slot { + pid_t pid; /* pid of the spawned reception process */ + uschar *host_address; /* address of the client host */ +} smtp_slot; + +/* An empty slot for initializing (Standard C does not allow constructor +expressions in assignments except as initializers in declarations). */ + +static smtp_slot empty_smtp_slot = { .pid = 0, .host_address = NULL }; + + + +/************************************************* +* Local static variables * +*************************************************/ + +static SIGNAL_BOOL sigchld_seen; +static SIGNAL_BOOL sighup_seen; +static SIGNAL_BOOL sigterm_seen; + +static int accept_retry_count = 0; +static int accept_retry_errno; +static BOOL accept_retry_select_failed; + +static int queue_run_count = 0; +static pid_t *queue_pid_slots = NULL; +static smtp_slot *smtp_slots = NULL; + +static BOOL write_pid = TRUE; + + + +/************************************************* +* SIGHUP Handler * +*************************************************/ + +/* All this handler does is to set a flag and re-enable the signal. + +Argument: the signal number +Returns: nothing +*/ + +static void +sighup_handler(int sig) +{ +sighup_seen = TRUE; +signal(SIGHUP, sighup_handler); +} + + + +/************************************************* +* SIGCHLD handler for main daemon process * +*************************************************/ + +/* Don't re-enable the handler here, since we aren't doing the +waiting here. If the signal is re-enabled, there will just be an +infinite sequence of calls to this handler. The SIGCHLD signal is +used just as a means of waking up the daemon so that it notices +terminated subprocesses as soon as possible. + +Argument: the signal number +Returns: nothing +*/ + +static void +main_sigchld_handler(int sig) +{ +os_non_restarting_signal(SIGCHLD, SIG_DFL); +sigchld_seen = TRUE; +} + + +/* SIGTERM handler. Try to get the daemon pid file removed +before exiting. */ + +static void +main_sigterm_handler(int sig) +{ +sigterm_seen = TRUE; +} + + + + +/************************************************* +* Unexpected errors in SMTP calls * +*************************************************/ + +/* This function just saves a bit of repetitious coding. + +Arguments: + log_msg Text of message to be logged + smtp_msg Text of SMTP error message + was_errno The failing errno + +Returns: nothing +*/ + +static void +never_error(uschar *log_msg, uschar *smtp_msg, int was_errno) +{ +uschar *emsg = was_errno <= 0 + ? US"" : string_sprintf(": %s", strerror(was_errno)); +log_write(0, LOG_MAIN|LOG_PANIC, "%s%s", log_msg, emsg); +if (smtp_out) smtp_printf("421 %s\r\n", FALSE, smtp_msg); +} + + + + +/************************************************* +*************************************************/ + +#ifndef EXIM_HAVE_ABSTRACT_UNIX_SOCKETS +static void +unlink_notifier_socket(void) +{ +uschar * s = expand_string(notifier_socket); +DEBUG(D_any) debug_printf("unlinking notifier socket %s\n", s); +Uunlink(s); +} +#endif + + +static void +close_daemon_sockets(int daemon_notifier_fd, + struct pollfd * fd_polls, int listen_socket_count) +{ +if (daemon_notifier_fd >= 0) + { + (void) close(daemon_notifier_fd); + daemon_notifier_fd = -1; +#ifndef EXIM_HAVE_ABSTRACT_UNIX_SOCKETS + unlink_notifier_socket(); +#endif + } + +for (int i = 0; i < listen_socket_count; i++) (void) close(fd_polls[i].fd); +} + + +/************************************************* +* Handle a connected SMTP call * +*************************************************/ + +/* This function is called when an SMTP connection has been accepted. +If there are too many, give an error message and close down. Otherwise +spin off a sub-process to handle the call. The list of listening sockets +is required so that they can be closed in the sub-process. Take care not to +leak store in this process - reset the stacking pool at the end. + +Arguments: + fd_polls sockets which are listening for incoming calls + listen_socket_count count of listening sockets + accept_socket socket of the current accepted call + accepted socket information about the current call + +Returns: nothing +*/ + +static void +handle_smtp_call(struct pollfd *fd_polls, int listen_socket_count, + int accept_socket, struct sockaddr *accepted) +{ +pid_t pid; +union sockaddr_46 interface_sockaddr; +EXIM_SOCKLEN_T ifsize = sizeof(interface_sockaddr); +int dup_accept_socket = -1; +int max_for_this_host = 0; +int save_log_selector = *log_selector; +gstring * whofrom; + +rmark reset_point = store_mark(); + +/* Make the address available in ASCII representation, and also fish out +the remote port. */ + +sender_host_address = host_ntoa(-1, accepted, NULL, &sender_host_port); +DEBUG(D_any) debug_printf("Connection request from %s port %d\n", + sender_host_address, sender_host_port); + +/* Set up the output stream, check the socket has duplicated, and set up the +input stream. These operations fail only the exceptional circumstances. Note +that never_error() won't use smtp_out if it is NULL. */ + +if (!(smtp_out = fdopen(accept_socket, "wb"))) + { + never_error(US"daemon: fdopen() for smtp_out failed", US"", errno); + goto ERROR_RETURN; + } + +if ((dup_accept_socket = dup(accept_socket)) < 0) + { + never_error(US"daemon: couldn't dup socket descriptor", + US"Connection setup failed", errno); + goto ERROR_RETURN; + } + +if (!(smtp_in = fdopen(dup_accept_socket, "rb"))) + { + never_error(US"daemon: fdopen() for smtp_in failed", + US"Connection setup failed", errno); + goto ERROR_RETURN; + } + +/* Get the data for the local interface address. Panic for most errors, but +"connection reset by peer" just means the connection went away. */ + +if (getsockname(accept_socket, (struct sockaddr *)(&interface_sockaddr), + &ifsize) < 0) + { + log_write(0, LOG_MAIN | ((errno == ECONNRESET)? 0 : LOG_PANIC), + "getsockname() failed: %s", strerror(errno)); + smtp_printf("421 Local problem: getsockname() failed; please try again later\r\n", FALSE); + goto ERROR_RETURN; + } + +interface_address = host_ntoa(-1, &interface_sockaddr, NULL, &interface_port); +DEBUG(D_interface) debug_printf("interface address=%s port=%d\n", + interface_address, interface_port); + +/* Build a string identifying the remote host and, if requested, the port and +the local interface data. This is for logging; at the end of this function the +memory is reclaimed. */ + +whofrom = string_append(NULL, 3, "[", sender_host_address, "]"); + +if (LOGGING(incoming_port)) + whofrom = string_fmt_append(whofrom, ":%d", sender_host_port); + +if (LOGGING(incoming_interface)) + whofrom = string_fmt_append(whofrom, " I=[%s]:%d", + interface_address, interface_port); + +(void) string_from_gstring(whofrom); /* Terminate the newly-built string */ + +/* Check maximum number of connections. We do not check for reserved +connections or unacceptable hosts here. That is done in the subprocess because +it might take some time. */ + +if (smtp_accept_max > 0 && smtp_accept_count >= smtp_accept_max) + { + DEBUG(D_any) debug_printf("rejecting SMTP connection: count=%d max=%d\n", + smtp_accept_count, smtp_accept_max); + smtp_printf("421 Too many concurrent SMTP connections; " + "please try again later.\r\n", FALSE); + log_write(L_connection_reject, + LOG_MAIN, "Connection from %s refused: too many connections", + whofrom->s); + goto ERROR_RETURN; + } + +/* If a load limit above which only reserved hosts are acceptable is defined, +get the load average here, and if there are in fact no reserved hosts, do +the test right away (saves a fork). If there are hosts, do the check in the +subprocess because it might take time. */ + +if (smtp_load_reserve >= 0) + { + load_average = OS_GETLOADAVG(); + if (!smtp_reserve_hosts && load_average > smtp_load_reserve) + { + DEBUG(D_any) debug_printf("rejecting SMTP connection: load average = %.2f\n", + (double)load_average/1000.0); + smtp_printf("421 Too much load; please try again later.\r\n", FALSE); + log_write(L_connection_reject, + LOG_MAIN, "Connection from %s refused: load average = %.2f", + whofrom->s, (double)load_average/1000.0); + goto ERROR_RETURN; + } + } + +/* Check that one specific host (strictly, IP address) is not hogging +resources. This is done here to prevent a denial of service attack by someone +forcing you to fork lots of times before denying service. The value of +smtp_accept_max_per_host is a string which is expanded. This makes it possible +to provide host-specific limits according to $sender_host address, but because +this is in the daemon mainline, only fast expansions (such as inline address +checks) should be used. The documentation is full of warnings. */ + +if (smtp_accept_max_per_host) + { + uschar *expanded = expand_string(smtp_accept_max_per_host); + if (!expanded) + { + if (!f.expand_string_forcedfail) + log_write(0, LOG_MAIN|LOG_PANIC, "expansion of smtp_accept_max_per_host " + "failed for %s: %s", whofrom->s, expand_string_message); + } + /* For speed, interpret a decimal number inline here */ + else + { + uschar *s = expanded; + while (isdigit(*s)) + max_for_this_host = max_for_this_host * 10 + *s++ - '0'; + if (*s) + log_write(0, LOG_MAIN|LOG_PANIC, "expansion of smtp_accept_max_per_host " + "for %s contains non-digit: %s", whofrom->s, expanded); + } + } + +/* If we have fewer connections than max_for_this_host, we can skip the tedious +per host_address checks. Note that at this stage smtp_accept_count contains the +count of *other* connections, not including this one. */ + +if (max_for_this_host > 0 && smtp_accept_count >= max_for_this_host) + { + int host_accept_count = 0; + int other_host_count = 0; /* keep a count of non matches to optimise */ + + for (int i = 0; i < smtp_accept_max; ++i) + if (smtp_slots[i].host_address) + { + if (Ustrcmp(sender_host_address, smtp_slots[i].host_address) == 0) + host_accept_count++; + else + other_host_count++; + + /* Testing all these strings is expensive - see if we can drop out + early, either by hitting the target, or finding there are not enough + connections left to make the target. */ + + if ( host_accept_count >= max_for_this_host + || smtp_accept_count - other_host_count < max_for_this_host) + break; + } + + if (host_accept_count >= max_for_this_host) + { + DEBUG(D_any) debug_printf("rejecting SMTP connection: too many from this " + "IP address: count=%d max=%d\n", + host_accept_count, max_for_this_host); + smtp_printf("421 Too many concurrent SMTP connections " + "from this IP address; please try again later.\r\n", FALSE); + log_write(L_connection_reject, + LOG_MAIN, "Connection from %s refused: too many connections " + "from that IP address", whofrom->s); + search_tidyup(); + goto ERROR_RETURN; + } + } + +/* OK, the connection count checks have been passed. Before we can fork the +accepting process, we must first log the connection if requested. This logging +used to happen in the subprocess, but doing that means that the value of +smtp_accept_count can be out of step by the time it is logged. So we have to do +the logging here and accept the performance cost. Note that smtp_accept_count +hasn't yet been incremented to take account of this connection. + +In order to minimize the cost (because this is going to happen for every +connection), do a preliminary selector test here. This saves ploughing through +the generalized logging code each time when the selector is false. If the +selector is set, check whether the host is on the list for logging. If not, +arrange to unset the selector in the subprocess. */ + +if (LOGGING(smtp_connection)) + { + uschar *list = hosts_connection_nolog; + memset(sender_host_cache, 0, sizeof(sender_host_cache)); + if (list && verify_check_host(&list) == OK) + save_log_selector &= ~L_smtp_connection; + else + log_write(L_smtp_connection, LOG_MAIN, "SMTP connection from %s " + "(TCP/IP connection count = %d)", whofrom->s, smtp_accept_count + 1); + } + +/* Now we can fork the accepting process; do a lookup tidy, just in case any +expansion above did a lookup. */ + +search_tidyup(); +pid = exim_fork(US"daemon-accept"); + +/* Handle the child process */ + +if (pid == 0) + { + int queue_only_reason = 0; + int old_pool = store_pool; + int save_debug_selector = debug_selector; + BOOL local_queue_only; + BOOL session_local_queue_only; +#ifdef SA_NOCLDWAIT + struct sigaction act; +#endif + + smtp_accept_count++; /* So that it includes this process */ + + /* If the listen backlog was over the monitoring level, log it. */ + + if (smtp_listen_backlog > smtp_backlog_monitor) + log_write(0, LOG_MAIN, "listen backlog %d I=[%s]:%d", + smtp_listen_backlog, interface_address, interface_port); + + /* May have been modified for the subprocess */ + + *log_selector = save_log_selector; + + /* Get the local interface address into permanent store */ + + store_pool = POOL_PERM; + interface_address = string_copy(interface_address); + store_pool = old_pool; + + /* Check for a tls-on-connect port */ + + if (host_is_tls_on_connect_port(interface_port)) tls_in.on_connect = TRUE; + + /* Expand smtp_active_hostname if required. We do not do this any earlier, + because it may depend on the local interface address (indeed, that is most + likely what it depends on.) */ + + smtp_active_hostname = primary_hostname; + if (raw_active_hostname) + { + uschar * nah = expand_string(raw_active_hostname); + if (!nah) + { + if (!f.expand_string_forcedfail) + { + log_write(0, LOG_MAIN|LOG_PANIC, "failed to expand \"%s\" " + "(smtp_active_hostname): %s", raw_active_hostname, + expand_string_message); + smtp_printf("421 Local configuration error; " + "please try again later.\r\n", FALSE); + mac_smtp_fflush(); + search_tidyup(); + exim_underbar_exit(EXIT_FAILURE); + } + } + else if (*nah) smtp_active_hostname = nah; + } + + /* Initialize the queueing flags */ + + queue_check_only(); + session_local_queue_only = queue_only; + + /* Close the listening sockets, and set the SIGCHLD handler to SIG_IGN. + We also attempt to set things up so that children are automatically reaped, + but just in case this isn't available, there's a paranoid waitpid() in the + loop too (except for systems where we are sure it isn't needed). See the more + extensive comment before the reception loop in exim.c for a fuller + explanation of this logic. */ + + close_daemon_sockets(daemon_notifier_fd, fd_polls, listen_socket_count); + + /* Set FD_CLOEXEC on the SMTP socket. We don't want any rogue child processes + to be able to communicate with them, under any circumstances. */ + (void)fcntl(accept_socket, F_SETFD, + fcntl(accept_socket, F_GETFD) | FD_CLOEXEC); + (void)fcntl(dup_accept_socket, F_SETFD, + fcntl(dup_accept_socket, F_GETFD) | FD_CLOEXEC); + +#ifdef SA_NOCLDWAIT + act.sa_handler = SIG_IGN; + sigemptyset(&(act.sa_mask)); + act.sa_flags = SA_NOCLDWAIT; + sigaction(SIGCHLD, &act, NULL); +#else + signal(SIGCHLD, SIG_IGN); +#endif + signal(SIGTERM, SIG_DFL); + signal(SIGINT, SIG_DFL); + + /* Attempt to get an id from the sending machine via the RFC 1413 + protocol. We do this in the sub-process in order not to hold up the + main process if there is any delay. Then set up the fullhost information + in case there is no HELO/EHLO. + + If debugging is enabled only for the daemon, we must turn if off while + finding the id, but turn it on again afterwards so that information about the + incoming connection is output. */ + + if (f.debug_daemon) debug_selector = 0; + verify_get_ident(IDENT_PORT); + host_build_sender_fullhost(); + debug_selector = save_debug_selector; + + DEBUG(D_any) + debug_printf("Process %d is handling incoming connection from %s\n", + (int)getpid(), sender_fullhost); + + /* Now disable debugging permanently if it's required only for the daemon + process. */ + + if (f.debug_daemon) debug_selector = 0; + + /* If there are too many child processes for immediate delivery, + set the session_local_queue_only flag, which is initialized from the + configured value and may therefore already be TRUE. Leave logging + till later so it will have a message id attached. Note that there is no + possibility of re-calculating this per-message, because the value of + smtp_accept_count does not change in this subprocess. */ + + if (smtp_accept_queue > 0 && smtp_accept_count > smtp_accept_queue) + { + session_local_queue_only = TRUE; + queue_only_reason = 1; + } + + /* Handle the start of the SMTP session, then loop, accepting incoming + messages from the SMTP connection. The end will come at the QUIT command, + when smtp_setup_msg() returns 0. A break in the connection causes the + process to die (see accept.c). + + NOTE: We do *not* call smtp_log_no_mail() if smtp_start_session() fails, + because a log line has already been written for all its failure exists + (usually "connection refused: ") and writing another one is + unnecessary clutter. */ + + if (!smtp_start_session()) + { + mac_smtp_fflush(); + search_tidyup(); + exim_underbar_exit(EXIT_SUCCESS); + } + + for (;;) + { + int rc; + message_id[0] = 0; /* Clear out any previous message_id */ + reset_point = store_mark(); /* Save current store high water point */ + + DEBUG(D_any) + debug_printf("Process %d is ready for new message\n", (int)getpid()); + + /* Smtp_setup_msg() returns 0 on QUIT or if the call is from an + unacceptable host or if an ACL "drop" command was triggered, -1 on + connection lost, and +1 on validly reaching DATA. Receive_msg() almost + always returns TRUE when smtp_input is true; just retry if no message was + accepted (can happen for invalid message parameters). However, it can yield + FALSE if the connection was forcibly dropped by the DATA ACL. */ + + if ((rc = smtp_setup_msg()) > 0) + { + BOOL ok = receive_msg(FALSE); + search_tidyup(); /* Close cached databases */ + if (!ok) /* Connection was dropped */ + { + cancel_cutthrough_connection(TRUE, US"receive dropped"); + mac_smtp_fflush(); + smtp_log_no_mail(); /* Log no mail if configured */ + exim_underbar_exit(EXIT_SUCCESS); + } + if (message_id[0] == 0) continue; /* No message was accepted */ + } + else /* bad smtp_setup_msg() */ + { + if (smtp_out) + { + int fd = fileno(smtp_in); + uschar buf[128]; + + mac_smtp_fflush(); + /* drain socket, for clean TCP FINs */ + if (fcntl(fd, F_SETFL, O_NONBLOCK) == 0) + for(int i = 16; read(fd, buf, sizeof(buf)) > 0 && i > 0; ) i--; + } + cancel_cutthrough_connection(TRUE, US"message setup dropped"); + search_tidyup(); + smtp_log_no_mail(); /* Log no mail if configured */ + + /*XXX should we pause briefly, hoping that the client will be the + active TCP closer hence get the TCP_WAIT endpoint? */ + DEBUG(D_receive) debug_printf("SMTP>>(close on process exit)\n"); + exim_underbar_exit(rc ? EXIT_FAILURE : EXIT_SUCCESS); + } + + /* Show the recipients when debugging */ + + DEBUG(D_receive) + { + if (sender_address) + debug_printf("Sender: %s\n", sender_address); + if (recipients_list) + { + debug_printf("Recipients:\n"); + for (int i = 0; i < recipients_count; i++) + debug_printf(" %s\n", recipients_list[i].address); + } + } + + /* A message has been accepted. Clean up any previous delivery processes + that have completed and are defunct, on systems where they don't go away + by themselves (see comments when setting SIG_IGN above). On such systems + (if any) these delivery processes hang around after termination until + the next message is received. */ + + #ifndef SIG_IGN_WORKS + while (waitpid(-1, NULL, WNOHANG) > 0); + #endif + + /* Reclaim up the store used in accepting this message */ + + { + int r = receive_messagecount; + BOOL q = f.queue_only_policy; + smtp_reset(reset_point); + reset_point = NULL; + f.queue_only_policy = q; + receive_messagecount = r; + } + + /* If queue_only is set or if there are too many incoming connections in + existence, session_local_queue_only will be TRUE. If it is not, check + whether we have received too many messages in this session for immediate + delivery. */ + + if (!session_local_queue_only && + smtp_accept_queue_per_connection > 0 && + receive_messagecount > smtp_accept_queue_per_connection) + { + session_local_queue_only = TRUE; + queue_only_reason = 2; + } + + /* Initialize local_queue_only from session_local_queue_only. If it is not + true, and queue_only_load is set, check that the load average is below it. + If local_queue_only is set by this means, we also set if for the session if + queue_only_load_latch is true (the default). This means that, once set, + local_queue_only remains set for any subsequent messages on the same SMTP + connection. This is a deliberate choice; even though the load average may + fall, it doesn't seem right to deliver later messages on the same call when + not delivering earlier ones. However, the are special circumstances such as + very long-lived connections from scanning appliances where this is not the + best strategy. In such cases, queue_only_load_latch should be set false. */ + + if ( !(local_queue_only = session_local_queue_only) + && queue_only_load >= 0 + && (local_queue_only = (load_average = OS_GETLOADAVG()) > queue_only_load) + ) + { + queue_only_reason = 3; + if (queue_only_load_latch) session_local_queue_only = TRUE; + } + + /* Log the queueing here, when it will get a message id attached, but + not if queue_only is set (case 0). */ + + if (local_queue_only) switch(queue_only_reason) + { + case 1: log_write(L_delay_delivery, + LOG_MAIN, "no immediate delivery: too many connections " + "(%d, max %d)", smtp_accept_count, smtp_accept_queue); + break; + + case 2: log_write(L_delay_delivery, + LOG_MAIN, "no immediate delivery: more than %d messages " + "received in one connection", smtp_accept_queue_per_connection); + break; + + case 3: log_write(L_delay_delivery, + LOG_MAIN, "no immediate delivery: load average %.2f", + (double)load_average/1000.0); + break; + } + + /* If a delivery attempt is required, spin off a new process to handle it. + If we are not root, we have to re-exec exim unless deliveries are being + done unprivileged. */ + + else if ( (!f.queue_only_policy || f.queue_smtp) + && !f.deliver_freeze) + { + pid_t dpid; + + /* We used to flush smtp_out before forking so that buffered data was not + duplicated, but now we want to pipeline the responses for data and quit. + Instead, hard-close the fd underlying smtp_out right after fork to discard + the data buffer. */ + + if ((dpid = exim_fork(US"daemon-accept-delivery")) == 0) + { + (void)fclose(smtp_in); + (void)close(fileno(smtp_out)); + (void)fclose(smtp_out); + smtp_in = smtp_out = NULL; + + /* Don't ever molest the parent's SSL connection, but do clean up + the data structures if necessary. */ + +#ifndef DISABLE_TLS + tls_close(NULL, TLS_NO_SHUTDOWN); +#endif + + /* Reset SIGHUP and SIGCHLD in the child in both cases. */ + + signal(SIGHUP, SIG_DFL); + signal(SIGCHLD, SIG_DFL); + signal(SIGTERM, SIG_DFL); + signal(SIGINT, SIG_DFL); + + if (geteuid() != root_uid && !deliver_drop_privilege) + { + signal(SIGALRM, SIG_DFL); + delivery_re_exec(CEE_EXEC_PANIC); + /* Control does not return here. */ + } + + /* No need to re-exec; SIGALRM remains set to the default handler */ + + (void) deliver_message(message_id, FALSE, FALSE); + search_tidyup(); + exim_underbar_exit(EXIT_SUCCESS); + } + + if (dpid > 0) + { + release_cutthrough_connection(US"passed for delivery"); + DEBUG(D_any) debug_printf("forked delivery process %d\n", (int)dpid); + } + else + { + cancel_cutthrough_connection(TRUE, US"delivery fork failed"); + log_write(0, LOG_MAIN|LOG_PANIC, "daemon: delivery process fork " + "failed: %s", strerror(errno)); + } + } + } + } + + +/* Carrying on in the parent daemon process... Can't do much if the fork +failed. Otherwise, keep count of the number of accepting processes and +remember the pid for ticking off when the child completes. */ + +if (pid < 0) + never_error(US"daemon: accept process fork failed", US"Fork failed", errno); +else + { + for (int i = 0; i < smtp_accept_max; ++i) + if (smtp_slots[i].pid <= 0) + { + smtp_slots[i].pid = pid; + /* Connection closes come asyncronously, so we cannot stack this store */ + if (smtp_accept_max_per_host) + smtp_slots[i].host_address = string_copy_malloc(sender_host_address); + smtp_accept_count++; + break; + } + DEBUG(D_any) debug_printf("%d SMTP accept process%s running\n", + smtp_accept_count, smtp_accept_count == 1 ? "" : "es"); + } + +/* Get here via goto in error cases */ + +ERROR_RETURN: + +/* Close the streams associated with the socket which will also close the +socket fds in this process. We can't do anything if fclose() fails, but +logging brings it to someone's attention. However, "connection reset by peer" +isn't really a problem, so skip that one. On Solaris, a dropped connection can +manifest itself as a broken pipe, so drop that one too. If the streams don't +exist, something went wrong while setting things up. Make sure the socket +descriptors are closed, in order to drop the connection. */ + +if (smtp_out) + { + if (fclose(smtp_out) != 0 && errno != ECONNRESET && errno != EPIPE) + log_write(0, LOG_MAIN|LOG_PANIC, "daemon: fclose(smtp_out) failed: %s", + strerror(errno)); + smtp_out = NULL; + } +else (void)close(accept_socket); + +if (smtp_in) + { + if (fclose(smtp_in) != 0 && errno != ECONNRESET && errno != EPIPE) + log_write(0, LOG_MAIN|LOG_PANIC, "daemon: fclose(smtp_in) failed: %s", + strerror(errno)); + smtp_in = NULL; + } +else (void)close(dup_accept_socket); + +/* Release any store used in this process, including the store used for holding +the incoming host address and an expanded active_hostname. */ + +log_close_all(); +interface_address = sender_host_name = sender_host_address = NULL; +store_reset(reset_point); +} + + + + +/************************************************* +* Check wildcard listen special cases * +*************************************************/ + +/* This function is used when binding and listening on lists of addresses and +ports. It tests for special cases of wildcard listening, when IPv4 and IPv6 +sockets may interact in different ways in different operating systems. It is +passed an error number, the list of listening addresses, and the current +address. Two checks are available: for a previous wildcard IPv6 address, or for +a following wildcard IPv4 address, in both cases on the same port. + +In practice, pairs of wildcard addresses should be adjacent in the address list +because they are sorted that way below. + +Arguments: + eno the error number + addresses the list of addresses + ipa the current IP address + back if TRUE, check for previous wildcard IPv6 address + if FALSE, check for a following wildcard IPv4 address + +Returns: TRUE or FALSE +*/ + +static BOOL +check_special_case(int eno, ip_address_item *addresses, ip_address_item *ipa, + BOOL back) +{ +ip_address_item *ipa2; + +/* For the "back" case, if the failure was "address in use" for a wildcard IPv4 +address, seek a previous IPv6 wildcard address on the same port. As it is +previous, it must have been successfully bound and be listening. Flag it as a +"6 including 4" listener. */ + +if (back) + { + if (eno != EADDRINUSE || ipa->address[0] != 0) return FALSE; + for (ipa2 = addresses; ipa2 != ipa; ipa2 = ipa2->next) + { + if (ipa2->address[1] == 0 && ipa2->port == ipa->port) + { + ipa2->v6_include_v4 = TRUE; + return TRUE; + } + } + } + +/* For the "forward" case, if the current address is a wildcard IPv6 address, +we seek a following wildcard IPv4 address on the same port. */ + +else + { + if (ipa->address[0] != ':' || ipa->address[1] != 0) return FALSE; + for (ipa2 = ipa->next; ipa2 != NULL; ipa2 = ipa2->next) + if (ipa2->address[0] == 0 && ipa->port == ipa2->port) return TRUE; + } + +return FALSE; +} + + + + +/************************************************* +* Handle terminating subprocesses * +*************************************************/ + +/* Handle the termination of child processes. Theoretically, this need be done +only when sigchld_seen is TRUE, but rumour has it that some systems lose +SIGCHLD signals at busy times, so to be on the safe side, this function is +called each time round. It shouldn't be too expensive. + +Arguments: none +Returns: nothing +*/ + +static void +handle_ending_processes(void) +{ +int status; +pid_t pid; + +while ((pid = waitpid(-1, &status, WNOHANG)) > 0) + { + DEBUG(D_any) + { + debug_printf("child %d ended: status=0x%x\n", (int)pid, status); +#ifdef WCOREDUMP + if (WIFEXITED(status)) + debug_printf(" normal exit, %d\n", WEXITSTATUS(status)); + else if (WIFSIGNALED(status)) + debug_printf(" signal exit, signal %d%s\n", WTERMSIG(status), + WCOREDUMP(status) ? " (core dumped)" : ""); +#endif + } + + /* If it's a listening daemon for which we are keeping track of individual + subprocesses, deal with an accepting process that has terminated. */ + + if (smtp_slots) + { + int i; + for (i = 0; i < smtp_accept_max; i++) + if (smtp_slots[i].pid == pid) + { + if (smtp_slots[i].host_address) + store_free(smtp_slots[i].host_address); + smtp_slots[i] = empty_smtp_slot; + if (--smtp_accept_count < 0) smtp_accept_count = 0; + DEBUG(D_any) debug_printf("%d SMTP accept process%s now running\n", + smtp_accept_count, (smtp_accept_count == 1)? "" : "es"); + break; + } + if (i < smtp_accept_max) continue; /* Found an accepting process */ + } + + /* If it wasn't an accepting process, see if it was a queue-runner + process that we are tracking. */ + + if (queue_pid_slots) + { + int max = atoi(CS expand_string(queue_run_max)); + for (int i = 0; i < max; i++) + if (queue_pid_slots[i] == pid) + { + queue_pid_slots[i] = 0; + if (--queue_run_count < 0) queue_run_count = 0; + DEBUG(D_any) debug_printf("%d queue-runner process%s now running\n", + queue_run_count, (queue_run_count == 1)? "" : "es"); + break; + } + } + } +} + + +static void +set_pid_file_path(void) +{ +if (override_pid_file_path) + pid_file_path = override_pid_file_path; + +if (!*pid_file_path) + pid_file_path = string_sprintf("%s/exim-daemon.pid", spool_directory); + +if (pid_file_path[0] != '/') + log_write(0, LOG_PANIC_DIE, "pid file path %s must be absolute\n", pid_file_path); +} + + +enum pid_op { PID_WRITE, PID_CHECK, PID_DELETE }; + +/* Do various pid file operations as safe as possible. Ideally we'd just +drop the privileges for creation of the pid file and not care at all about removal of +the file. FIXME. +Returns: true on success, false + errno==EACCES otherwise +*/ + +static BOOL +operate_on_pid_file(const enum pid_op operation, const pid_t pid) +{ +char pid_line[sizeof(int) * 3 + 2]; +const int pid_len = snprintf(pid_line, sizeof(pid_line), "%d\n", (int)pid); +BOOL lines_match = FALSE; +uschar * path, * base, * dir; + +const int dir_flags = O_RDONLY | O_NONBLOCK; +const int base_flags = O_NOFOLLOW | O_NONBLOCK; +const mode_t base_mode = 0644; +struct stat sb; +int cwd_fd = -1, dir_fd = -1, base_fd = -1; +BOOL success = FALSE; +errno = EACCES; + +set_pid_file_path(); +if (!f.running_in_test_harness && real_uid != root_uid && real_uid != exim_uid) goto cleanup; +if (pid_len < 2 || pid_len >= (int)sizeof(pid_line)) goto cleanup; + +path = string_copy(pid_file_path); +if ((base = Ustrrchr(path, '/')) == NULL) /* should not happen, but who knows */ + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "pid file path \"%s\" does not contain a '/'", pid_file_path); + +dir = base != path ? path : US"/"; +*base++ = '\0'; + +if (!dir || !*dir || *dir != '/') goto cleanup; +if (!base || !*base || Ustrchr(base, '/') != NULL) goto cleanup; + +cwd_fd = open(".", dir_flags); +if (cwd_fd < 0 || fstat(cwd_fd, &sb) != 0 || !S_ISDIR(sb.st_mode)) goto cleanup; +dir_fd = open(CS dir, dir_flags); +if (dir_fd < 0 || fstat(dir_fd, &sb) != 0 || !S_ISDIR(sb.st_mode)) goto cleanup; + +/* emulate openat */ +if (fchdir(dir_fd) != 0) goto cleanup; +base_fd = open(CS base, O_RDONLY | base_flags); +if (fchdir(cwd_fd) != 0) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "can't return to previous working dir: %s", strerror(errno)); + +if (base_fd >= 0) + { + char line[sizeof(pid_line)]; + ssize_t len = -1; + + if (fstat(base_fd, &sb) != 0 || !S_ISREG(sb.st_mode)) goto cleanup; + if ((sb.st_mode & 07777) != base_mode || sb.st_nlink != 1) goto cleanup; + if (sb.st_size < 2 || sb.st_size >= (off_t)sizeof(line)) goto cleanup; + + len = read(base_fd, line, sizeof(line)); + if (len != (ssize_t)sb.st_size) goto cleanup; + line[len] = '\0'; + + if (strspn(line, "0123456789") != (size_t)len-1) goto cleanup; + if (line[len-1] != '\n') goto cleanup; + lines_match = len == pid_len && strcmp(line, pid_line) == 0; + } + +if (operation == PID_WRITE) + { + if (!lines_match) + { + if (base_fd >= 0) + { + int error = -1; + /* emulate unlinkat */ + if (fchdir(dir_fd) != 0) goto cleanup; + error = unlink(CS base); + if (fchdir(cwd_fd) != 0) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "can't return to previous working dir: %s", strerror(errno)); + if (error) goto cleanup; + (void)close(base_fd); + base_fd = -1; + } + /* emulate openat */ + if (fchdir(dir_fd) != 0) goto cleanup; + base_fd = open(CS base, O_WRONLY | O_CREAT | O_EXCL | base_flags, base_mode); + if (fchdir(cwd_fd) != 0) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "can't return to previous working dir: %s", strerror(errno)); + if (base_fd < 0) goto cleanup; + if (fchmod(base_fd, base_mode) != 0) goto cleanup; + if (write(base_fd, pid_line, pid_len) != pid_len) goto cleanup; + DEBUG(D_any) debug_printf("pid written to %s\n", pid_file_path); + } + } +else + { + if (!lines_match) goto cleanup; + if (operation == PID_DELETE) + { + int error = -1; + /* emulate unlinkat */ + if (fchdir(dir_fd) != 0) goto cleanup; + error = unlink(CS base); + if (fchdir(cwd_fd) != 0) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "can't return to previous working dir: %s", strerror(errno)); + if (error) goto cleanup; + } + } + +success = TRUE; +errno = 0; + +cleanup: +if (cwd_fd >= 0) (void)close(cwd_fd); +if (dir_fd >= 0) (void)close(dir_fd); +if (base_fd >= 0) (void)close(base_fd); +return success; +} + + +/* Remove the daemon's pidfile. Note: runs with root privilege, +as a direct child of the daemon. Does not return. */ + +void +delete_pid_file(void) +{ +const BOOL success = operate_on_pid_file(PID_DELETE, getppid()); + +DEBUG(D_any) + debug_printf("delete pid file %s %s: %s\n", pid_file_path, + success ? "success" : "failure", strerror(errno)); + +exim_exit(EXIT_SUCCESS); +} + + +/* Called by the daemon; exec a child to get the pid file deleted +since we may require privs for the containing directory */ + +static void +daemon_die(void) +{ +int pid; + +DEBUG(D_any) debug_printf("SIGTERM/SIGINT seen\n"); +#if !defined(DISABLE_TLS) && (defined(EXIM_HAVE_INOTIFY) || defined(EXIM_HAVE_KEVENT)) +tls_watch_invalidate(); +#endif + +if (daemon_notifier_fd >= 0) + { + close(daemon_notifier_fd); + daemon_notifier_fd = -1; +#ifndef EXIM_HAVE_ABSTRACT_UNIX_SOCKETS + unlink_notifier_socket(); +#endif + } + +if (f.running_in_test_harness || write_pid) + { + if ((pid = exim_fork(US"daemon-del-pidfile")) == 0) + { + if (override_pid_file_path) + (void)child_exec_exim(CEE_EXEC_PANIC, FALSE, NULL, FALSE, 3, + "-oP", override_pid_file_path, "-oPX"); + else + (void)child_exec_exim(CEE_EXEC_PANIC, FALSE, NULL, FALSE, 1, "-oPX"); + + /* Control never returns here. */ + } + if (pid > 0) + child_close(pid, 1); + } +exim_exit(EXIT_SUCCESS); +} + + +/************************************************* +* Listener socket for local work prompts * +*************************************************/ + +static void +daemon_notifier_socket(void) +{ +int fd; +const uschar * where; +struct sockaddr_un sa_un = {.sun_family = AF_UNIX}; +int len; + +if (!notifier_socket || !*notifier_socket) + { + DEBUG(D_any) debug_printf("-oY used so not creating notifier socket\n"); + return; + } +if (override_local_interfaces && !override_pid_file_path) + { + DEBUG(D_any) + debug_printf("-oX used without -oP so not creating notifier socket\n"); + return; + } + +DEBUG(D_any) debug_printf("creating notifier socket\n"); + +#ifdef SOCK_CLOEXEC +if ((fd = socket(PF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0)) < 0) + { where = US"socket"; goto bad; } +#else +if ((fd = socket(PF_UNIX, SOCK_DGRAM, 0)) < 0) + { where = US"socket"; goto bad; } +(void)fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) | FD_CLOEXEC); +#endif + +#ifdef EXIM_HAVE_ABSTRACT_UNIX_SOCKETS +sa_un.sun_path[0] = 0; /* Abstract local socket addr - Linux-specific? */ +len = offsetof(struct sockaddr_un, sun_path) + 1 + + snprintf(sa_un.sun_path+1, sizeof(sa_un.sun_path)-1, "%s", + expand_string(notifier_socket)); +DEBUG(D_any) debug_printf(" @%s\n", sa_un.sun_path+1); +#else /* filesystem-visible and persistent; will neeed removal */ +len = offsetof(struct sockaddr_un, sun_path) + + snprintf(sa_un.sun_path, sizeof(sa_un.sun_path), "%s", + expand_string(notifier_socket)); +DEBUG(D_any) debug_printf(" %s\n", sa_un.sun_path); +#endif + +if (bind(fd, (const struct sockaddr *)&sa_un, len) < 0) + { where = US"bind"; goto bad; } + +#ifdef SO_PASSCRED /* Linux */ +if (setsockopt(fd, SOL_SOCKET, SO_PASSCRED, &on, sizeof(on)) < 0) + { where = US"SO_PASSCRED"; goto bad2; } +#elif defined(LOCAL_CREDS) /* FreeBSD-ish */ +if (setsockopt(fd, 0, LOCAL_CREDS, &on, sizeof(on)) < 0) + { where = US"LOCAL_CREDS"; goto bad2; } +#endif + +/* debug_printf("%s: fd %d\n", __FUNCTION__, fd); */ +daemon_notifier_fd = fd; +return; + +bad2: +#ifndef EXIM_HAVE_ABSTRACT_UNIX_SOCKETS + Uunlink(sa_un.sun_path); +#endif +bad: + log_write(0, LOG_MAIN|LOG_PANIC, "%s %s: %s", + __FUNCTION__, where, strerror(errno)); + close(fd); + return; +} + + +static uschar queuerun_msgid[MESSAGE_ID_LENGTH+1]; + +/* Return TRUE if a sigalrm should be emulated */ +static BOOL +daemon_notification(void) +{ +uschar buf[256], cbuf[256]; +struct sockaddr_un sa_un; +struct iovec iov = {.iov_base = buf, .iov_len = sizeof(buf)-1}; +struct msghdr msg = { .msg_name = &sa_un, + .msg_namelen = sizeof(sa_un), + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_control = cbuf, + .msg_controllen = sizeof(cbuf) + }; +ssize_t sz; + +buf[sizeof(buf)-1] = 0; +if ((sz = recvmsg(daemon_notifier_fd, &msg, 0)) <= 0) return FALSE; +if (sz >= sizeof(buf)) return FALSE; + +#ifdef notdef +debug_printf("addrlen %d\n", msg.msg_namelen); +#endif +DEBUG(D_queue_run) debug_printf("%s from addr '%s%.*s'\n", __FUNCTION__, + *sa_un.sun_path ? "" : "@", + (int)msg.msg_namelen - (*sa_un.sun_path ? 0 : 1), + sa_un.sun_path + (*sa_un.sun_path ? 0 : 1)); + +/* Refuse to handle the item unless the peer has good credentials */ +#ifdef SCM_CREDENTIALS +# define EXIM_SCM_CR_TYPE SCM_CREDENTIALS +#elif defined(LOCAL_CREDS) && defined(SCM_CREDS) +# define EXIM_SCM_CR_TYPE SCM_CREDS +#else + /* The OS has no way to get the creds of the caller (for a unix/datagram socket. + Punt; don't try to check. */ +#endif + +#ifdef EXIM_SCM_CR_TYPE +for (struct cmsghdr * cp = CMSG_FIRSTHDR(&msg); + cp; + cp = CMSG_NXTHDR(&msg, cp)) + if (cp->cmsg_level == SOL_SOCKET && cp->cmsg_type == EXIM_SCM_CR_TYPE) + { +# ifdef SCM_CREDENTIALS /* Linux */ + struct ucred * cr = (struct ucred *) CMSG_DATA(cp); + if (cr->uid && cr->uid != exim_uid) + { + DEBUG(D_queue_run) debug_printf("%s: sender creds pid %d uid %d gid %d\n", + __FUNCTION__, (int)cr->pid, (int)cr->uid, (int)cr->gid); + return FALSE; + } +# elif defined(LOCAL_CREDS) /* BSD-ish */ + struct sockcred * cr = (struct sockcred *) CMSG_DATA(cp); + if (cr->sc_uid && cr->sc_uid != exim_uid) + { + DEBUG(D_queue_run) debug_printf("%s: sender creds pid ??? uid %d gid %d\n", + __FUNCTION__, (int)cr->sc_uid, (int)cr->sc_gid); + return FALSE; + } +# endif + break; + } +#endif + +buf[sz] = 0; +switch (buf[0]) + { +#ifndef DISABLE_QUEUE_RAMP + case NOTIFY_MSG_QRUN: + /* this should be a message_id */ + DEBUG(D_queue_run) + debug_printf("%s: qrunner trigger: %s\n", __FUNCTION__, buf+1); + memcpy(queuerun_msgid, buf+1, MESSAGE_ID_LENGTH+1); + return TRUE; +#endif + + case NOTIFY_QUEUE_SIZE_REQ: + { + uschar buf[16]; + int len = snprintf(CS buf, sizeof(buf), "%u", queue_count_cached()); + + DEBUG(D_queue_run) + debug_printf("%s: queue size request: %s\n", __FUNCTION__, buf); + + if (sendto(daemon_notifier_fd, buf, len, 0, + (const struct sockaddr *)&sa_un, msg.msg_namelen) < 0) + log_write(0, LOG_MAIN|LOG_PANIC, + "%s: sendto: %s\n", __FUNCTION__, strerror(errno)); + return FALSE; + } + } +return FALSE; +} + + + + +/************************************************* +* Exim Daemon Mainline * +*************************************************/ + +/* The daemon can do two jobs, either of which is optional: + +(1) Listens for incoming SMTP calls and spawns off a sub-process to handle +each one. This is requested by the -bd option, with -oX specifying the SMTP +port on which to listen (for testing). + +(2) Spawns a queue-running process every so often. This is controlled by the +-q option with a an interval time. (If no time is given, a single queue run +is done from the main function, and control doesn't get here.) + +Root privilege is required in order to attach to port 25. Some systems require +it when calling socket() rather than bind(). To cope with all cases, we run as +root for both socket() and bind(). Some systems also require root in order to +write to the pid file directory. This function must therefore be called as root +if it is to work properly in all circumstances. Once the socket is bound and +the pid file written, root privilege is given up if there is an exim uid. + +There are no arguments to this function, and it never returns. */ + +void +daemon_go(void) +{ +struct passwd * pw; +struct pollfd * fd_polls, * tls_watch_poll = NULL, * dnotify_poll = NULL; +int listen_socket_count = 0, poll_fd_count; +ip_address_item * addresses = NULL; +time_t last_connection_time = (time_t)0; +int local_queue_run_max = atoi(CS expand_string(queue_run_max)); + +process_purpose = US"daemon"; + +/* If any debugging options are set, turn on the D_pid bit so that all +debugging lines get the pid added. */ + +DEBUG(D_any|D_v) debug_selector |= D_pid; + +/* Allocate enough pollstructs for inetd mode plus the ancillary sockets; +also used when there are no listen sockets. */ + +fd_polls = store_get(sizeof(struct pollfd) * 3, GET_UNTAINTED); + +if (f.inetd_wait_mode) + { + listen_socket_count = 1; + (void) close(3); + if (dup2(0, 3) == -1) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, + "failed to dup inetd socket safely away: %s", strerror(errno)); + + fd_polls[0].fd = 3; + fd_polls[0].events = POLLIN; + (void) close(0); + (void) close(1); + (void) close(2); + exim_nullstd(); + + if (debug_file == stderr) + { + /* need a call to log_write before call to open debug_file, so that + log.c:file_path has been initialised. This is unfortunate. */ + log_write(0, LOG_MAIN, "debugging Exim in inetd wait mode starting"); + + fclose(debug_file); + debug_file = NULL; + exim_nullstd(); /* re-open fd2 after we just closed it again */ + debug_logging_activate(US"-wait", NULL); + } + + DEBUG(D_any) debug_printf("running in inetd wait mode\n"); + + /* As per below, when creating sockets ourselves, we handle tcp_nodelay for + our own buffering; we assume though that inetd set the socket REUSEADDR. */ + + if (tcp_nodelay) + if (setsockopt(3, IPPROTO_TCP, TCP_NODELAY, US &on, sizeof(on))) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "failed to set socket NODELAY: %s", + strerror(errno)); + } + + +if (f.inetd_wait_mode || f.daemon_listen) + { + /* If any option requiring a load average to be available during the + reception of a message is set, call os_getloadavg() while we are root + for those OS for which this is necessary the first time it is called (in + order to perform an "open" on the kernel memory file). */ + +#ifdef LOAD_AVG_NEEDS_ROOT + if (queue_only_load >= 0 || smtp_load_reserve >= 0 || + (deliver_queue_load_max >= 0 && deliver_drop_privilege)) + (void)os_getloadavg(); +#endif + } + + +/* Do the preparation for setting up a listener on one or more interfaces, and +possible on various ports. This is controlled by the combination of +local_interfaces (which can set IP addresses and ports) and daemon_smtp_port +(which is a list of default ports to use for those items in local_interfaces +that do not specify a port). The -oX command line option can be used to +override one or both of these options. + +If local_interfaces is not set, the default is to listen on all interfaces. +When it is set, it can include "all IPvx interfaces" as an item. This is useful +when different ports are in use. + +It turns out that listening on all interfaces is messy in an IPv6 world, +because several different implementation approaches have been taken. This code +is now supposed to work with all of them. The point of difference is whether an +IPv6 socket that is listening on all interfaces will receive incoming IPv4 +calls or not. We also have to cope with the case when IPv6 libraries exist, but +there is no IPv6 support in the kernel. + +. On Solaris, an IPv6 socket will accept IPv4 calls, and give them as mapped + addresses. However, if an IPv4 socket is also listening on all interfaces, + calls are directed to the appropriate socket. + +. On (some versions of) Linux, an IPv6 socket will accept IPv4 calls, and + give them as mapped addresses, but an attempt also to listen on an IPv4 + socket on all interfaces causes an error. + +. On OpenBSD, an IPv6 socket will not accept IPv4 calls. You have to set up + two sockets if you want to accept both kinds of call. + +. FreeBSD is like OpenBSD, but it has the IPV6_V6ONLY socket option, which + can be turned off, to make it behave like the versions of Linux described + above. + +. I heard a report that the USAGI IPv6 stack for Linux has implemented + IPV6_V6ONLY. + +So, what we do when IPv6 is supported is as follows: + + (1) After it is set up, the list of interfaces is scanned for wildcard + addresses. If an IPv6 and an IPv4 wildcard are both found for the same + port, the list is re-arranged so that they are together, with the IPv6 + wildcard first. + + (2) If the creation of a wildcard IPv6 socket fails, we just log the error and + carry on if an IPv4 wildcard socket for the same port follows later in the + list. This allows Exim to carry on in the case when the kernel has no IPv6 + support. + + (3) Having created an IPv6 wildcard socket, we try to set IPV6_V6ONLY if that + option is defined. However, if setting fails, carry on regardless (but log + the incident). + + (4) If binding or listening on an IPv6 wildcard socket fails, it is a serious + error. + + (5) If binding or listening on an IPv4 wildcard socket fails with the error + EADDRINUSE, and a previous interface was an IPv6 wildcard for the same + port (which must have succeeded or we wouldn't have got this far), we + assume we are in the situation where just a single socket is permitted, + and ignore the error. + +Phew! + +The preparation code decodes options and sets up the relevant data. We do this +first, so that we can return non-zero if there are any syntax errors, and also +write to stderr. */ + +if (f.daemon_listen && !f.inetd_wait_mode) + { + int *default_smtp_port; + int sep; + int pct = 0; + uschar *s; + const uschar * list; + uschar *local_iface_source = US"local_interfaces"; + ip_address_item *ipa; + ip_address_item **pipa; + + /* If -oX was used, disable the writing of a pid file unless -oP was + explicitly used to force it. Then scan the string given to -oX. Any items + that contain neither a dot nor a colon are used to override daemon_smtp_port. + Any other items are used to override local_interfaces. */ + + if (override_local_interfaces) + { + gstring * new_smtp_port = NULL; + gstring * new_local_interfaces = NULL; + + if (!override_pid_file_path) write_pid = FALSE; + + list = override_local_interfaces; + sep = 0; + while ((s = string_nextinlist(&list, &sep, NULL, 0))) + { + uschar joinstr[4]; + gstring ** gp = Ustrpbrk(s, ".:") ? &new_local_interfaces : &new_smtp_port; + + if (!*gp) + { + joinstr[0] = sep; + joinstr[1] = ' '; + *gp = string_catn(*gp, US"<", 1); + } + + *gp = string_catn(*gp, joinstr, 2); + *gp = string_cat (*gp, s); + } + + if (new_smtp_port) + { + daemon_smtp_port = string_from_gstring(new_smtp_port); + DEBUG(D_any) debug_printf("daemon_smtp_port overridden by -oX:\n %s\n", + daemon_smtp_port); + } + + if (new_local_interfaces) + { + local_interfaces = string_from_gstring(new_local_interfaces); + local_iface_source = US"-oX data"; + DEBUG(D_any) debug_printf("local_interfaces overridden by -oX:\n %s\n", + local_interfaces); + } + } + + /* Create a list of default SMTP ports, to be used if local_interfaces + contains entries without explicit ports. First count the number of ports, then + build a translated list in a vector. */ + + list = daemon_smtp_port; + sep = 0; + while ((s = string_nextinlist(&list, &sep, NULL, 0))) + pct++; + default_smtp_port = store_get((pct+1) * sizeof(int), GET_UNTAINTED); + list = daemon_smtp_port; + sep = 0; + for (pct = 0; + (s = string_nextinlist(&list, &sep, NULL, 0)); + pct++) + { + if (isdigit(*s)) + { + uschar *end; + default_smtp_port[pct] = Ustrtol(s, &end, 0); + if (end != s + Ustrlen(s)) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG, "invalid SMTP port: %s", s); + } + else + { + struct servent *smtp_service = getservbyname(CS s, "tcp"); + if (!smtp_service) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG, "TCP port \"%s\" not found", s); + default_smtp_port[pct] = ntohs(smtp_service->s_port); + } + } + default_smtp_port[pct] = 0; + + /* Check the list of TLS-on-connect ports and do name lookups if needed */ + + list = tls_in.on_connect_ports; + sep = 0; + /* the list isn't expanded so cannot be tainted. If it ever is we will trap here */ + while ((s = string_nextinlist(&list, &sep, big_buffer, big_buffer_size))) + if (!isdigit(*s)) + { + gstring * g = NULL; + + list = tls_in.on_connect_ports; + tls_in.on_connect_ports = NULL; + sep = 0; + while ((s = string_nextinlist(&list, &sep, big_buffer, big_buffer_size))) + { + if (!isdigit(*s)) + { + struct servent * smtp_service = getservbyname(CS s, "tcp"); + if (!smtp_service) + log_write(0, LOG_PANIC_DIE|LOG_CONFIG, "TCP port \"%s\" not found", s); + s = string_sprintf("%d", (int)ntohs(smtp_service->s_port)); + } + g = string_append_listele(g, ':', s); + } + if (g) + tls_in.on_connect_ports = g->s; + break; + } + + /* Create the list of local interfaces, possibly with ports included. This + list may contain references to 0.0.0.0 and ::0 as wildcards. These special + values are converted below. */ + + addresses = host_build_ifacelist(local_interfaces, local_iface_source); + + /* In the list of IP addresses, convert 0.0.0.0 into an empty string, and ::0 + into the string ":". We use these to recognize wildcards in IPv4 and IPv6. In + fact, many IP stacks recognize 0.0.0.0 and ::0 and handle them as wildcards + anyway, but we need to know which are the wildcard addresses, and the shorter + strings are neater. + + In the same scan, fill in missing port numbers from the default list. When + there is more than one item in the list, extra items are created. */ + + for (ipa = addresses; ipa; ipa = ipa->next) + { + if (Ustrcmp(ipa->address, "0.0.0.0") == 0) + ipa->address[0] = 0; + else if (Ustrcmp(ipa->address, "::0") == 0) + { + ipa->address[0] = ':'; + ipa->address[1] = 0; + } + + if (ipa->port > 0) continue; + + if (daemon_smtp_port[0] <= 0) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "no port specified for interface " + "%s and daemon_smtp_port is unset; cannot start daemon", + ipa->address[0] == 0 ? US"\"all IPv4\"" : + ipa->address[1] == 0 ? US"\"all IPv6\"" : ipa->address); + + ipa->port = default_smtp_port[0]; + for (int i = 1; default_smtp_port[i] > 0; i++) + { + ip_address_item * new = store_get(sizeof(ip_address_item), GET_UNTAINTED); + + memcpy(new->address, ipa->address, Ustrlen(ipa->address) + 1); + new->port = default_smtp_port[i]; + new->next = ipa->next; + ipa->next = new; + ipa = new; + } + } + + /* Scan the list of addresses for wildcards. If we find an IPv4 and an IPv6 + wildcard for the same port, ensure that (a) they are together and (b) the + IPv6 address comes first. This makes handling the messy features easier, and + also simplifies the construction of the "daemon started" log line. */ + + pipa = &addresses; + for (ipa = addresses; ipa; pipa = &ipa->next, ipa = ipa->next) + { + ip_address_item *ipa2; + + /* Handle an IPv4 wildcard */ + + if (ipa->address[0] == 0) + for (ipa2 = ipa; ipa2->next; ipa2 = ipa2->next) + { + ip_address_item *ipa3 = ipa2->next; + if (ipa3->address[0] == ':' && + ipa3->address[1] == 0 && + ipa3->port == ipa->port) + { + ipa2->next = ipa3->next; + ipa3->next = ipa; + *pipa = ipa3; + break; + } + } + + /* Handle an IPv6 wildcard. */ + + else if (ipa->address[0] == ':' && ipa->address[1] == 0) + for (ipa2 = ipa; ipa2->next; ipa2 = ipa2->next) + { + ip_address_item *ipa3 = ipa2->next; + if (ipa3->address[0] == 0 && ipa3->port == ipa->port) + { + ipa2->next = ipa3->next; + ipa3->next = ipa->next; + ipa->next = ipa3; + ipa = ipa3; + break; + } + } + } + + /* Get a vector to remember all the sockets in. + Two extra elements for the ancillary sockets */ + + for (ipa = addresses; ipa; ipa = ipa->next) + listen_socket_count++; + fd_polls = store_get(sizeof(struct pollfd) * (listen_socket_count + 2), + GET_UNTAINTED); + for (struct pollfd * p = fd_polls; p < fd_polls + listen_socket_count + 2; + p++) + { p->fd = -1; p->events = POLLIN; } + + } /* daemon_listen but not inetd_wait_mode */ + +if (f.daemon_listen) + { + + /* Do a sanity check on the max connects value just to save us from getting + a huge amount of store. */ + + if (smtp_accept_max > 4095) smtp_accept_max = 4096; + + /* There's no point setting smtp_accept_queue unless it is less than the max + connects limit. The configuration reader ensures that the max is set if the + queue-only option is set. */ + + if (smtp_accept_queue > smtp_accept_max) smtp_accept_queue = 0; + + /* Get somewhere to keep the list of SMTP accepting pids if we are keeping + track of them for total number and queue/host limits. */ + + if (smtp_accept_max > 0) + { + smtp_slots = store_get(smtp_accept_max * sizeof(smtp_slot), GET_UNTAINTED); + for (int i = 0; i < smtp_accept_max; i++) smtp_slots[i] = empty_smtp_slot; + } + } + +/* The variable background_daemon is always false when debugging, but +can also be forced false in order to keep a non-debugging daemon in the +foreground. If background_daemon is true, close all open file descriptors that +we know about, but then re-open stdin, stdout, and stderr to /dev/null. Also +do this for inetd_wait mode. + +This is protection against any called functions (in libraries, or in +Perl, or whatever) that think they can write to stderr (or stdout). Before this +was added, it was quite likely that an SMTP connection would use one of these +file descriptors, in which case writing random stuff to it caused chaos. + +Then disconnect from the controlling terminal, Most modern Unixes seem to have +setsid() for getting rid of the controlling terminal. For any OS that doesn't, +setsid() can be #defined as a no-op, or as something else. */ + +if (f.background_daemon || f.inetd_wait_mode) + { + log_close_all(); /* Just in case anything was logged earlier */ + search_tidyup(); /* Just in case any were used in reading the config. */ + (void)close(0); /* Get rid of stdin/stdout/stderr */ + (void)close(1); + (void)close(2); + exim_nullstd(); /* Connect stdin/stdout/stderr to /dev/null */ + log_stderr = NULL; /* So no attempt to copy paniclog output */ + } + +if (f.background_daemon) + { + /* If the parent process of this one has pid == 1, we are re-initializing the + daemon as the result of a SIGHUP. In this case, there is no need to do + anything, because the controlling terminal has long gone. Otherwise, fork, in + case current process is a process group leader (see 'man setsid' for an + explanation) before calling setsid(). */ + + if (getppid() != 1) + { + pid_t pid = exim_fork(US"daemon"); + if (pid < 0) log_write(0, LOG_MAIN|LOG_PANIC_DIE, + "fork() failed when starting daemon: %s", strerror(errno)); + if (pid > 0) exit(EXIT_SUCCESS); /* in parent process, just exit */ + (void)setsid(); /* release controlling terminal */ + } + } + +/* We are now in the disconnected, daemon process (unless debugging). Set up +the listening sockets if required. */ + +daemon_notifier_socket(); + +if (f.daemon_listen && !f.inetd_wait_mode) + { + int sk; + ip_address_item *ipa; + + /* For each IP address, create a socket, bind it to the appropriate port, and + start listening. See comments above about IPv6 sockets that may or may not + accept IPv4 calls when listening on all interfaces. We also have to cope with + the case of a system with IPv6 libraries, but no IPv6 support in the kernel. + listening, provided a wildcard IPv4 socket for the same port follows. */ + + for (ipa = addresses, sk = 0; sk < listen_socket_count; ipa = ipa->next, sk++) + { + BOOL wildcard; + ip_address_item * ipa2; + int fd, af; + + if (Ustrchr(ipa->address, ':') != NULL) + { + af = AF_INET6; + wildcard = ipa->address[1] == 0; + } + else + { + af = AF_INET; + wildcard = ipa->address[0] == 0; + } + + if ((fd_polls[sk].fd = fd = ip_socket(SOCK_STREAM, af)) < 0) + { + if (check_special_case(0, addresses, ipa, FALSE)) + { + log_write(0, LOG_MAIN, "Failed to create IPv6 socket for wildcard " + "listening (%s): will use IPv4", strerror(errno)); + goto SKIP_SOCKET; + } + log_write(0, LOG_PANIC_DIE, "IPv%c socket creation failed: %s", + af == AF_INET6 ? '6' : '4', strerror(errno)); + } + + /* If this is an IPv6 wildcard socket, set IPV6_V6ONLY if that option is + available. Just log failure (can get protocol not available, just like + socket creation can). */ + +#ifdef IPV6_V6ONLY + if (af == AF_INET6 && wildcard && + setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)) < 0) + log_write(0, LOG_MAIN, "Setting IPV6_V6ONLY on daemon's IPv6 wildcard " + "socket failed (%s): carrying on without it", strerror(errno)); +#endif /* IPV6_V6ONLY */ + + /* Set SO_REUSEADDR so that the daemon can be restarted while a connection + is being handled. Without this, a connection will prevent reuse of the + smtp port for listening. */ + + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "setting SO_REUSEADDR on socket " + "failed when starting daemon: %s", strerror(errno)); + + /* Set TCP_NODELAY; Exim does its own buffering. There is a switch to + disable this because it breaks some broken clients. */ + + if (tcp_nodelay) setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)); + + /* Now bind the socket to the required port; if Exim is being restarted + it may not always be possible to bind immediately, even with SO_REUSEADDR + set, so try 10 times, waiting between each try. After 10 failures, we give + up. In an IPv6 environment, if bind () fails with the error EADDRINUSE and + we are doing wildcard IPv4 listening and there was a previous IPv6 wildcard + address for the same port, ignore the error on the grounds that we must be + in a system where the IPv6 socket accepts both kinds of call. This is + necessary for (some release of) USAGI Linux; other IP stacks fail at the + listen() stage instead. */ + +#ifdef TCP_FASTOPEN + f.tcp_fastopen_ok = TRUE; +#endif + for(;;) + { + uschar *msg, *addr; + if (ip_bind(fd, af, ipa->address, ipa->port) >= 0) break; + if (check_special_case(errno, addresses, ipa, TRUE)) + { + DEBUG(D_any) debug_printf("wildcard IPv4 bind() failed after IPv6 " + "listen() success; EADDRINUSE ignored\n"); + (void)close(fd); + goto SKIP_SOCKET; + } + msg = US strerror(errno); + addr = wildcard + ? af == AF_INET6 + ? US"(any IPv6)" + : US"(any IPv4)" + : ipa->address; + if (daemon_startup_retries <= 0) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, + "socket bind() to port %d for address %s failed: %s: " + "daemon abandoned", ipa->port, addr, msg); + log_write(0, LOG_MAIN, "socket bind() to port %d for address %s " + "failed: %s: waiting %s before trying again (%d more %s)", + ipa->port, addr, msg, readconf_printtime(daemon_startup_sleep), + daemon_startup_retries, (daemon_startup_retries > 1)? "tries" : "try"); + daemon_startup_retries--; + sleep(daemon_startup_sleep); + } + + DEBUG(D_any) + if (wildcard) + debug_printf("listening on all interfaces (IPv%c) port %d\n", + af == AF_INET6 ? '6' : '4', ipa->port); + else + debug_printf("listening on %s port %d\n", ipa->address, ipa->port); + + /* Start listening on the bound socket, establishing the maximum backlog of + connections that is allowed. On success, add to the set of sockets for select + and continue to the next address. */ + +#if defined(TCP_FASTOPEN) && !defined(__APPLE__) + if ( f.tcp_fastopen_ok + && setsockopt(fd, IPPROTO_TCP, TCP_FASTOPEN, + &smtp_connect_backlog, sizeof(smtp_connect_backlog))) + { + DEBUG(D_any) debug_printf("setsockopt FASTOPEN: %s\n", strerror(errno)); + f.tcp_fastopen_ok = FALSE; + } +#endif + if (listen(fd, smtp_connect_backlog) >= 0) + { +#if defined(TCP_FASTOPEN) && defined(__APPLE__) + if ( f.tcp_fastopen_ok + && setsockopt(fd, IPPROTO_TCP, TCP_FASTOPEN, &on, sizeof(on))) + { + DEBUG(D_any) debug_printf("setsockopt FASTOPEN: %s\n", strerror(errno)); + f.tcp_fastopen_ok = FALSE; + } +#endif + fd_polls[sk].fd = fd; + continue; + } + + /* Listening has failed. In an IPv6 environment, as for bind(), if listen() + fails with the error EADDRINUSE and we are doing IPv4 wildcard listening + and there was a previous successful IPv6 wildcard listen on the same port, + we want to ignore the error on the grounds that we must be in a system + where the IPv6 socket accepts both kinds of call. */ + + if (!check_special_case(errno, addresses, ipa, TRUE)) + log_write(0, LOG_PANIC_DIE, "listen() failed on interface %s: %s", + wildcard + ? af == AF_INET6 ? US"(any IPv6)" : US"(any IPv4)" : ipa->address, + strerror(errno)); + + DEBUG(D_any) debug_printf("wildcard IPv4 listen() failed after IPv6 " + "listen() success; EADDRINUSE ignored\n"); + (void)close(fd); + + /* Come here if there has been a problem with the socket which we + are going to ignore. We remove the address from the chain, and back up the + counts. */ + + SKIP_SOCKET: + sk--; /* Back up the count */ + listen_socket_count--; /* Reduce the total */ + if (ipa == addresses) addresses = ipa->next; else + { + for (ipa2 = addresses; ipa2->next != ipa; ipa2 = ipa2->next); + ipa2->next = ipa->next; + ipa = ipa2; + } + } /* End of bind/listen loop for each address */ + } /* End of setup for listening */ + + +/* If we are not listening, we want to write a pid file only if -oP was +explicitly given. */ + +else if (!override_pid_file_path) + write_pid = FALSE; + +/* Write the pid to a known file for assistance in identification, if required. +We do this before giving up root privilege, because on some systems it is +necessary to be root in order to write into the pid file directory. There's +nothing to stop multiple daemons running, as long as no more than one listens +on a given TCP/IP port on the same interface(s). However, in these +circumstances it gets far too complicated to mess with pid file names +automatically. Consequently, Exim 4 writes a pid file only + + (a) When running in the test harness, or + (b) When -bd is used and -oX is not used, or + (c) When -oP is used to supply a path. + +The variable daemon_write_pid is used to control this. */ + +if (f.running_in_test_harness || write_pid) + { + const enum pid_op operation = (f.running_in_test_harness + || real_uid == root_uid + || (real_uid == exim_uid && !override_pid_file_path)) ? PID_WRITE : PID_CHECK; + if (!operate_on_pid_file(operation, getpid())) + DEBUG(D_any) debug_printf("%s pid file %s: %s\n", (operation == PID_WRITE) ? "write" : "check", pid_file_path, strerror(errno)); + } + +/* Set up the handler for SIGHUP, which causes a restart of the daemon. */ + +sighup_seen = FALSE; +signal(SIGHUP, sighup_handler); + +/* Give up root privilege at this point (assuming that exim_uid and exim_gid +are not root). The third argument controls the running of initgroups(). +Normally we do this, in order to set up the groups for the Exim user. However, +if we are not root at this time - some odd installations run that way - we +cannot do this. */ + +exim_setugid(exim_uid, exim_gid, geteuid()==root_uid, US"running as a daemon"); + +/* Update the originator_xxx fields so that received messages as listed as +coming from Exim, not whoever started the daemon. */ + +originator_uid = exim_uid; +originator_gid = exim_gid; +originator_login = (pw = getpwuid(exim_uid)) + ? string_copy_perm(US pw->pw_name, FALSE) : US"exim"; + +/* Get somewhere to keep the list of queue-runner pids if we are keeping track +of them (and also if we are doing queue runs). */ + +if (queue_interval > 0 && local_queue_run_max > 0) + { + queue_pid_slots = store_get(local_queue_run_max * sizeof(pid_t), GET_UNTAINTED); + for (int i = 0; i < local_queue_run_max; i++) queue_pid_slots[i] = 0; + } + +/* Set up the handler for termination of child processes, and the one +telling us to die. */ + +sigchld_seen = FALSE; +os_non_restarting_signal(SIGCHLD, main_sigchld_handler); + +sigterm_seen = FALSE; +os_non_restarting_signal(SIGTERM, main_sigterm_handler); +os_non_restarting_signal(SIGINT, main_sigterm_handler); + +/* If we are to run the queue periodically, pretend the alarm has just gone +off. This will cause the first queue-runner to get kicked off straight away. */ + +sigalrm_seen = (queue_interval > 0); + +/* Log the start up of a daemon - at least one of listening or queue running +must be set up. */ + +if (f.inetd_wait_mode) + { + uschar *p = big_buffer; + + if (inetd_wait_timeout >= 0) + sprintf(CS p, "terminating after %d seconds", inetd_wait_timeout); + else + sprintf(CS p, "with no wait timeout"); + + log_write(0, LOG_MAIN, + "exim %s daemon started: pid=%d, launched with listening socket, %s", + version_string, getpid(), big_buffer); + set_process_info("daemon(%s): pre-listening socket", version_string); + + /* set up the timeout logic */ + sigalrm_seen = TRUE; + } + +else if (f.daemon_listen) + { + int smtp_ports = 0; + int smtps_ports = 0; + ip_address_item * ipa; + uschar * p; + uschar * qinfo = queue_interval > 0 + ? string_sprintf("-q%s%s", + f.queue_2stage ? "q" : "", readconf_printtime(queue_interval)) + : US"no queue runs"; + + /* Build a list of listening addresses in big_buffer, but limit it to 10 + items. The style is for backwards compatibility. + + It is now possible to have some ports listening for SMTPS (the old, + deprecated protocol that starts TLS without using STARTTLS), and others + listening for standard SMTP. Keep their listings separate. */ + + for (int j = 0, i; j < 2; j++) + { + for (i = 0, ipa = addresses; i < 10 && ipa; i++, ipa = ipa->next) + { + /* First time round, look for SMTP ports; second time round, look for + SMTPS ports. Build IP+port strings. */ + + if (host_is_tls_on_connect_port(ipa->port) == (j > 0)) + { + if (j == 0) + smtp_ports++; + else + smtps_ports++; + + /* Now the information about the port (and sometimes interface) */ + + if (ipa->address[0] == ':' && ipa->address[1] == 0) + { /* v6 wildcard */ + if (ipa->next && ipa->next->address[0] == 0 && + ipa->next->port == ipa->port) + { + ipa->log = string_sprintf(" port %d (IPv6 and IPv4)", ipa->port); + (ipa = ipa->next)->log = NULL; + } + else if (ipa->v6_include_v4) + ipa->log = string_sprintf(" port %d (IPv6 with IPv4)", ipa->port); + else + ipa->log = string_sprintf(" port %d (IPv6)", ipa->port); + } + else if (ipa->address[0] == 0) /* v4 wildcard */ + ipa->log = string_sprintf(" port %d (IPv4)", ipa->port); + else /* check for previously-seen IP */ + { + ip_address_item * i2; + for (i2 = addresses; i2 != ipa; i2 = i2->next) + if ( host_is_tls_on_connect_port(i2->port) == (j > 0) + && Ustrcmp(ipa->address, i2->address) == 0 + ) + { /* found; append port to list */ + for (p = i2->log; *p; ) p++; /* end of existing string */ + if (*--p == '}') *p = '\0'; /* drop EOL */ + while (isdigit(*--p)) ; /* char before port */ + + i2->log = *p == ':' /* no list yet? */ + ? string_sprintf("%.*s{%s,%d}", + (int)(p - i2->log + 1), i2->log, p+1, ipa->port) + : string_sprintf("%s,%d}", i2->log, ipa->port); + ipa->log = NULL; + break; + } + if (i2 == ipa) /* first-time IP */ + ipa->log = string_sprintf(" [%s]:%d", ipa->address, ipa->port); + } + } + } + } + + p = big_buffer; + for (int j = 0, i; j < 2; j++) + { + /* First time round, look for SMTP ports; second time round, look for + SMTPS ports. For the first one of each, insert leading text. */ + + if (j == 0) + { + if (smtp_ports > 0) + p += sprintf(CS p, "SMTP on"); + } + else + if (smtps_ports > 0) + p += sprintf(CS p, "%sSMTPS on", + smtp_ports == 0 ? "" : " and for "); + + /* Now the information about the port (and sometimes interface) */ + + for (i = 0, ipa = addresses; i < 10 && ipa; i++, ipa = ipa->next) + if (host_is_tls_on_connect_port(ipa->port) == (j > 0)) + if (ipa->log) + p += sprintf(CS p, "%s", ipa->log); + + if (ipa) + p += sprintf(CS p, " ..."); + } + + log_write(0, LOG_MAIN, + "exim %s daemon started: pid=%d, %s, listening for %s", + version_string, getpid(), qinfo, big_buffer); + set_process_info("daemon(%s): %s, listening for %s", + version_string, qinfo, big_buffer); + } + +else + { + uschar * s = *queue_name + ? string_sprintf("-qG%s/%s", queue_name, readconf_printtime(queue_interval)) + : string_sprintf("-q%s", readconf_printtime(queue_interval)); + log_write(0, LOG_MAIN, + "exim %s daemon started: pid=%d, %s, not listening for SMTP", + version_string, getpid(), s); + set_process_info("daemon(%s): %s, not listening", version_string, s); + } + +/* Do any work it might be useful to amortize over our children +(eg: compile regex) */ + +dns_pattern_init(); +smtp_deliver_init(); /* Used for callouts */ + +#ifndef DISABLE_DKIM + { +# ifdef MEASURE_TIMING + struct timeval t0; + gettimeofday(&t0, NULL); +# endif + dkim_exim_init(); +# ifdef MEASURE_TIMING + report_time_since(&t0, US"dkim_exim_init (delta)"); +# endif + } +#endif + +#ifdef WITH_CONTENT_SCAN +malware_init(); +#endif +#ifdef SUPPORT_SPF +spf_init(); +#endif +#ifndef DISABLE_TLS +tls_daemon_init(); +#endif + +/* Add ancillary sockets to the set for select */ + +poll_fd_count = listen_socket_count; +#ifndef DISABLE_TLS +if (tls_watch_fd >= 0) + { + tls_watch_poll = &fd_polls[poll_fd_count++]; + tls_watch_poll->fd = tls_watch_fd; + tls_watch_poll->events = POLLIN; + } +#endif +if (daemon_notifier_fd >= 0) + { + dnotify_poll = &fd_polls[poll_fd_count++]; + dnotify_poll->fd = daemon_notifier_fd; + dnotify_poll->events = POLLIN; + } + +/* Close the log so it can be renamed and moved. In the few cases below where +this long-running process writes to the log (always exceptional conditions), it +closes the log afterwards, for the same reason. */ + +log_close_all(); + +DEBUG(D_any) debug_print_ids(US"daemon running with"); + +/* Any messages accepted via this route are going to be SMTP. */ + +smtp_input = TRUE; + +#ifdef MEASURE_TIMING +report_time_since(×tamp_startup, US"daemon loop start"); /* testcase 0022 */ +#endif + +/* Enter the never-ending loop... */ + +for (;;) + { + pid_t pid; + + if (sigterm_seen) + daemon_die(); /* Does not return */ + + /* This code is placed first in the loop, so that it gets obeyed at the + start, before the first wait, for the queue-runner case, so that the first + one can be started immediately. + + The other option is that we have an inetd wait timeout specified to -bw. */ + + if (sigalrm_seen) + { + if (inetd_wait_timeout > 0) + { + time_t resignal_interval = inetd_wait_timeout; + + if (last_connection_time == (time_t)0) + { + DEBUG(D_any) + debug_printf("inetd wait timeout expired, but still not seen first message, ignoring\n"); + } + else + { + time_t now = time(NULL); + if (now == (time_t)-1) + { + DEBUG(D_any) debug_printf("failed to get time: %s\n", strerror(errno)); + } + else + { + if ((now - last_connection_time) >= inetd_wait_timeout) + { + DEBUG(D_any) + debug_printf("inetd wait timeout %d expired, ending daemon\n", + inetd_wait_timeout); + log_write(0, LOG_MAIN, "exim %s daemon terminating, inetd wait timeout reached.\n", + version_string); + exit(EXIT_SUCCESS); + } + else + { + resignal_interval -= (now - last_connection_time); + } + } + } + + sigalrm_seen = FALSE; + ALARM(resignal_interval); + } + + else + { + DEBUG(D_any) debug_printf("%s received\n", +#ifndef DISABLE_QUEUE_RAMP + *queuerun_msgid ? "qrun notification" : +#endif + "SIGALRM"); + + /* Do a full queue run in a child process, if required, unless we already + have enough queue runners on the go. If we are not running as root, a + re-exec is required. */ + + if ( queue_interval > 0 + && (local_queue_run_max <= 0 || queue_run_count < local_queue_run_max)) + { + if ((pid = exim_fork(US"queue-runner")) == 0) + { + /* Disable debugging if it's required only for the daemon process. We + leave the above message, because it ties up with the "child ended" + debugging messages. */ + + if (f.debug_daemon) debug_selector = 0; + + /* Close any open listening sockets in the child */ + + close_daemon_sockets(daemon_notifier_fd, + fd_polls, listen_socket_count); + + /* Reset SIGHUP and SIGCHLD in the child in both cases. */ + + signal(SIGHUP, SIG_DFL); + signal(SIGCHLD, SIG_DFL); + signal(SIGTERM, SIG_DFL); + signal(SIGINT, SIG_DFL); + + /* Re-exec if privilege has been given up, unless deliver_drop_ + privilege is set. Reset SIGALRM before exec(). */ + + if (geteuid() != root_uid && !deliver_drop_privilege) + { + uschar opt[8]; + uschar *p = opt; + uschar *extra[7]; + int extracount = 1; + + signal(SIGALRM, SIG_DFL); + *p++ = '-'; + *p++ = 'q'; + if ( f.queue_2stage +#ifndef DISABLE_QUEUE_RAMP + && !*queuerun_msgid +#endif + ) *p++ = 'q'; + if (f.queue_run_first_delivery) *p++ = 'i'; + if (f.queue_run_force) *p++ = 'f'; + if (f.deliver_force_thaw) *p++ = 'f'; + if (f.queue_run_local) *p++ = 'l'; + *p = 0; + extra[0] = *queue_name + ? string_sprintf("%sG%s", opt, queue_name) : opt; + +#ifndef DISABLE_QUEUE_RAMP + if (*queuerun_msgid) + { + log_write(0, LOG_MAIN, "notify triggered queue run"); + extra[extracount++] = queuerun_msgid; /* Trigger only the */ + extra[extracount++] = queuerun_msgid; /* one message */ + } +#endif + + /* If -R or -S were on the original command line, ensure they get + passed on. */ + + if (deliver_selectstring) + { + extra[extracount++] = f.deliver_selectstring_regex ? US"-Rr" : US"-R"; + extra[extracount++] = deliver_selectstring; + } + + if (deliver_selectstring_sender) + { + extra[extracount++] = f.deliver_selectstring_sender_regex + ? US"-Sr" : US"-S"; + extra[extracount++] = deliver_selectstring_sender; + } + + /* Overlay this process with a new execution. */ + + (void)child_exec_exim(CEE_EXEC_PANIC, FALSE, NULL, FALSE, extracount, + extra[0], extra[1], extra[2], extra[3], extra[4], extra[5], extra[6]); + + /* Control never returns here. */ + } + + /* No need to re-exec; SIGALRM remains set to the default handler */ + +#ifndef DISABLE_QUEUE_RAMP + if (*queuerun_msgid) + { + log_write(0, LOG_MAIN, "notify triggered queue run"); + f.queue_2stage = FALSE; + queue_run(queuerun_msgid, queuerun_msgid, FALSE); + } + else +#endif + queue_run(NULL, NULL, FALSE); + exim_underbar_exit(EXIT_SUCCESS); + } + + if (pid < 0) + { + log_write(0, LOG_MAIN|LOG_PANIC, "daemon: fork of queue-runner " + "process failed: %s", strerror(errno)); + log_close_all(); + } + else + { + for (int i = 0; i < local_queue_run_max; ++i) + if (queue_pid_slots[i] <= 0) + { + queue_pid_slots[i] = pid; + queue_run_count++; + break; + } + DEBUG(D_any) debug_printf("%d queue-runner process%s running\n", + queue_run_count, queue_run_count == 1 ? "" : "es"); + } + } + + /* Reset the alarm clock */ + + sigalrm_seen = FALSE; +#ifndef DISABLE_QUEUE_RAMP + if (*queuerun_msgid) + *queuerun_msgid = 0; + else +#endif + ALARM(queue_interval); + } + + } /* sigalrm_seen */ + + + /* Sleep till a connection happens if listening, and handle the connection if + that is why we woke up. The FreeBSD operating system requires the use of + select() before accept() because the latter function is not interrupted by + a signal, and we want to wake up for SIGCHLD and SIGALRM signals. Some other + OS do notice signals in accept() but it does no harm to have the select() + in for all of them - and it won't then be a lurking problem for ports to + new OS. In fact, the later addition of listening on specific interfaces only + requires this way of working anyway. */ + + if (f.daemon_listen) + { + int lcount; + BOOL select_failed = FALSE; + + DEBUG(D_any) debug_printf("Listening...\n"); + + /* In rare cases we may have had a SIGCHLD signal in the time between + setting the handler (below) and getting back here. If so, pretend that the + select() was interrupted so that we reap the child. This might still leave + a small window when a SIGCHLD could get lost. However, since we use SIGCHLD + only to do the reaping more quickly, it shouldn't result in anything other + than a delay until something else causes a wake-up. */ + + if (sigchld_seen) + { + lcount = -1; + errno = EINTR; + } + else + lcount = poll(fd_polls, poll_fd_count, -1); + + if (lcount < 0) + { + select_failed = TRUE; + lcount = 1; + } + + /* Clean up any subprocesses that may have terminated. We need to do this + here so that smtp_accept_max_per_host works when a connection to that host + has completed, and we are about to accept a new one. When this code was + later in the sequence, a new connection could be rejected, even though an + old one had just finished. Preserve the errno from any select() failure for + the use of the common select/accept error processing below. */ + + { + int select_errno = errno; + handle_ending_processes(); + +#ifndef DISABLE_TLS + { + int old_tfd; + /* Create or rotate any required keys; handle (delayed) filewatch event */ + + if ((old_tfd = tls_daemon_tick()) >= 0) + for (struct pollfd * p = &fd_polls[listen_socket_count]; + p < fd_polls + poll_fd_count; p++) + if (p->fd == old_tfd) { p->fd = tls_watch_fd ; break; } + } +#endif + errno = select_errno; + } + + /* Loop for all the sockets that are currently ready to go. If select + actually failed, we have set the count to 1 and select_failed=TRUE, so as + to use the common error code for select/accept below. */ + + while (lcount-- > 0) + { + int accept_socket = -1; +#if HAVE_IPV6 + struct sockaddr_in6 accepted; +#else + struct sockaddr_in accepted; +#endif + + if (!select_failed) + { +#if !defined(DISABLE_TLS) && (defined(EXIM_HAVE_INOTIFY) || defined(EXIM_HAVE_KEVENT)) + if (tls_watch_poll && tls_watch_poll->revents & POLLIN) + { + tls_watch_poll->revents = 0; + tls_watch_trigger_time = time(NULL); /* Set up delayed event */ + tls_watch_discard_event(tls_watch_fd); + break; /* to top of daemon loop */ + } +#endif + if (dnotify_poll && dnotify_poll->revents & POLLIN) + { + dnotify_poll->revents = 0; + sigalrm_seen = daemon_notification(); + break; /* to top of daemon loop */ + } + for (struct pollfd * p = fd_polls; p < fd_polls + listen_socket_count; + p++) + if (p->revents & POLLIN) + { + EXIM_SOCKLEN_T alen = sizeof(accepted); +#ifdef TCP_INFO + struct tcp_info ti; + socklen_t tlen = sizeof(ti); + + /* If monitoring the backlog is wanted, grab for later logging */ + + smtp_listen_backlog = 0; + if ( smtp_backlog_monitor > 0 + && getsockopt(p->fd, IPPROTO_TCP, TCP_INFO, &ti, &tlen) == 0) + { +# ifdef EXIM_HAVE_TCPI_UNACKED + DEBUG(D_interface) debug_printf("listen fd %d queue max %u curr %u\n", + p->fd, ti.tcpi_sacked, ti.tcpi_unacked); + smtp_listen_backlog = ti.tcpi_unacked; +# elif defined(__FreeBSD__) /* This does not work. Investigate kernel sourcecode. */ + DEBUG(D_interface) debug_printf("listen fd %d queue max %u curr %u\n", + p->fd, ti.__tcpi_sacked, ti.__tcpi_unacked); + smtp_listen_backlog = ti.__tcpi_unacked; +# endif + } +#endif + p->revents = 0; + accept_socket = accept(p->fd, (struct sockaddr *)&accepted, &alen); + break; + } + } + + /* If select or accept has failed and this was not caused by an + interruption, log the incident and try again. With asymmetric TCP/IP + routing errors such as "No route to network" have been seen here. Also + "connection reset by peer" has been seen. These cannot be classed as + disastrous errors, but they could fill up a lot of log. The code in smail + crashes the daemon after 10 successive failures of accept, on the grounds + that some OS fail continuously. Exim originally followed suit, but this + appears to have caused problems. Now it just keeps going, but instead of + logging each error, it batches them up when they are continuous. */ + + if (accept_socket < 0 && errno != EINTR) + { + if (accept_retry_count == 0) + { + accept_retry_errno = errno; + accept_retry_select_failed = select_failed; + } + else if ( errno != accept_retry_errno + || select_failed != accept_retry_select_failed + || accept_retry_count >= 50) + { + log_write(0, LOG_MAIN | (accept_retry_count >= 50 ? LOG_PANIC : 0), + "%d %s() failure%s: %s", + accept_retry_count, + accept_retry_select_failed ? "select" : "accept", + accept_retry_count == 1 ? "" : "s", + strerror(accept_retry_errno)); + log_close_all(); + accept_retry_count = 0; + accept_retry_errno = errno; + accept_retry_select_failed = select_failed; + } + accept_retry_count++; + } + else if (accept_retry_count > 0) + { + log_write(0, LOG_MAIN, "%d %s() failure%s: %s", + accept_retry_count, + accept_retry_select_failed ? "select" : "accept", + accept_retry_count == 1 ? "" : "s", + strerror(accept_retry_errno)); + log_close_all(); + accept_retry_count = 0; + } + + /* If select/accept succeeded, deal with the connection. */ + + if (accept_socket >= 0) + { +#ifdef TCP_QUICKACK /* Avoid pure-ACKs while in tls protocol pingpong phase */ + /* Unfortunately we cannot be certain to do this before a TLS-on-connect + Client Hello arrives and is acked. We do it as early as possible. */ + (void) setsockopt(accept_socket, IPPROTO_TCP, TCP_QUICKACK, US &off, sizeof(off)); +#endif + if (inetd_wait_timeout) + last_connection_time = time(NULL); + handle_smtp_call(fd_polls, listen_socket_count, accept_socket, + (struct sockaddr *)&accepted); + } + } + } + + /* If not listening, then just sleep for the queue interval. If we woke + up early the last time for some other signal, it won't matter because + the alarm signal will wake at the right time. This code originally used + sleep() but it turns out that on the FreeBSD system, sleep() is not inter- + rupted by signals, so it wasn't waking up for SIGALRM or SIGCHLD. Luckily + select() can be used as an interruptible sleep() on all versions of Unix. */ + + else + { + struct pollfd p; + poll(&p, 0, queue_interval * 1000); + handle_ending_processes(); + } + + /* Re-enable the SIGCHLD handler if it has been run. It can't do it + for itself, because it isn't doing the waiting itself. */ + + if (sigchld_seen) + { + sigchld_seen = FALSE; + os_non_restarting_signal(SIGCHLD, main_sigchld_handler); + } + + /* Handle being woken by SIGHUP. We know at this point that the result + of accept() has been dealt with, so we can re-exec exim safely, first + closing the listening sockets so that they can be reused. Cancel any pending + alarm in case it is just about to go off, and set SIGHUP to be ignored so + that another HUP in quick succession doesn't clobber the new daemon before it + gets going. All log files get closed by the close-on-exec flag; however, if + the exec fails, we need to close the logs. */ + + if (sighup_seen) + { + log_write(0, LOG_MAIN, "pid %d: SIGHUP received: re-exec daemon", + getpid()); + close_daemon_sockets(daemon_notifier_fd, fd_polls, listen_socket_count); + ALARM_CLR(0); + signal(SIGHUP, SIG_IGN); + sighup_argv[0] = exim_path; + exim_nullstd(); + execv(CS exim_path, (char *const *)sighup_argv); + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "pid %d: exec of %s failed: %s", + getpid(), exim_path, strerror(errno)); + log_close_all(); + } + + } /* End of main loop */ + +/* Control never reaches here */ +} + +/* vi: aw ai sw=2 +*/ +/* End of exim_daemon.c */ diff --git a/src/dane-openssl.c b/src/dane-openssl.c new file mode 100644 index 0000000..6ed3529 --- /dev/null +++ b/src/dane-openssl.c @@ -0,0 +1,1719 @@ +/* + * Author: Viktor Dukhovni + * License: THIS CODE IS IN THE PUBLIC DOMAIN. + * + * Copyright (c) The Exim Maintainers 2014 - 2019 + */ +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if OPENSSL_VERSION_NUMBER < 0x1000000fL +# error "OpenSSL 1.0.0 or higher required" +#endif + +#if OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER) +# define X509_up_ref(x) CRYPTO_add(&((x)->references), 1, CRYPTO_LOCK_X509) +#endif + +/* LibreSSL 2.9.0 and later - 2.9.0 has removed a number of macros ... */ +#ifdef LIBRESSL_VERSION_NUMBER +# if LIBRESSL_VERSION_NUMBER >= 0x2090000fL +# define EXIM_HAVE_ASN1_MACROS +# endif +#endif +/* OpenSSL */ +#if OPENSSL_VERSION_NUMBER >= 0x10100000L && !defined(LIBRESSL_VERSION_NUMBER) +# define EXIM_HAVE_ASN1_MACROS +# define EXIM_OPAQUE_X509 +/* Older OpenSSL and all LibreSSL */ +#else +# define X509_STORE_CTX_get_verify(ctx) (ctx)->verify +# define X509_STORE_CTX_get_verify_cb(ctx) (ctx)->verify_cb +# define X509_STORE_CTX_get0_cert(ctx) (ctx)->cert +# define X509_STORE_CTX_get0_chain(ctx) (ctx)->chain +# define X509_STORE_CTX_get0_untrusted(ctx) (ctx)->untrusted + +# define X509_STORE_CTX_set_verify(ctx, verify_chain) (ctx)->verify = (verify_chain) +# define X509_STORE_CTX_set0_verified_chain(ctx, sk) (ctx)->chain = (sk) +# define X509_STORE_CTX_set_error_depth(ctx, val) (ctx)->error_depth = (val) +# define X509_STORE_CTX_set_current_cert(ctx, cert) (ctx)->current_cert = (cert) + +# define ASN1_STRING_get0_data ASN1_STRING_data +# define X509_getm_notBefore X509_get_notBefore +# define X509_getm_notAfter X509_get_notAfter + +# define CRYPTO_ONCE_STATIC_INIT 0 +# define CRYPTO_THREAD_run_once run_once +typedef int CRYPTO_ONCE; +#endif + + +#include "danessl.h" + +#define DANESSL_F_ADD_SKID 100 +#define DANESSL_F_ADD_TLSA 101 +#define DANESSL_F_CHECK_END_ENTITY 102 +#define DANESSL_F_CTX_INIT 103 +#define DANESSL_F_GROW_CHAIN 104 +#define DANESSL_F_INIT 105 +#define DANESSL_F_LIBRARY_INIT 106 +#define DANESSL_F_LIST_ALLOC 107 +#define DANESSL_F_MATCH 108 +#define DANESSL_F_PUSH_EXT 109 +#define DANESSL_F_SET_TRUST_ANCHOR 110 +#define DANESSL_F_VERIFY_CERT 111 +#define DANESSL_F_WRAP_CERT 112 +#define DANESSL_F_DANESSL_VERIFY_CHAIN 113 + +#define DANESSL_R_BAD_CERT 100 +#define DANESSL_R_BAD_CERT_PKEY 101 +#define DANESSL_R_BAD_DATA_LENGTH 102 +#define DANESSL_R_BAD_DIGEST 103 +#define DANESSL_R_BAD_NULL_DATA 104 +#define DANESSL_R_BAD_PKEY 105 +#define DANESSL_R_BAD_SELECTOR 106 +#define DANESSL_R_BAD_USAGE 107 +#define DANESSL_R_INIT 108 +#define DANESSL_R_LIBRARY_INIT 109 +#define DANESSL_R_NOSIGN_KEY 110 +#define DANESSL_R_SCTX_INIT 111 +#define DANESSL_R_SUPPORT 112 + +#ifndef OPENSSL_NO_ERR +#define DANESSL_F_PLACEHOLDER 0 /* FIRST! Value TBD */ +static ERR_STRING_DATA dane_str_functs[] = { + /* error string */ + {DANESSL_F_PLACEHOLDER, "DANE library"}, /* FIRST!!! */ + {DANESSL_F_ADD_SKID, "add_skid"}, + {DANESSL_F_ADD_TLSA, "DANESSL_add_tlsa"}, + {DANESSL_F_CHECK_END_ENTITY, "check_end_entity"}, + {DANESSL_F_CTX_INIT, "DANESSL_CTX_init"}, + {DANESSL_F_GROW_CHAIN, "grow_chain"}, + {DANESSL_F_INIT, "DANESSL_init"}, + {DANESSL_F_LIBRARY_INIT, "DANESSL_library_init"}, + {DANESSL_F_LIST_ALLOC, "list_alloc"}, + {DANESSL_F_MATCH, "match"}, + {DANESSL_F_PUSH_EXT, "push_ext"}, + {DANESSL_F_SET_TRUST_ANCHOR, "set_trust_anchor"}, + {DANESSL_F_VERIFY_CERT, "verify_cert"}, + {DANESSL_F_WRAP_CERT, "wrap_cert"}, + {0, NULL} +}; +static ERR_STRING_DATA dane_str_reasons[] = { + /* error string */ + {DANESSL_R_BAD_CERT, "Bad TLSA record certificate"}, + {DANESSL_R_BAD_CERT_PKEY, "Bad TLSA record certificate public key"}, + {DANESSL_R_BAD_DATA_LENGTH, "Bad TLSA record digest length"}, + {DANESSL_R_BAD_DIGEST, "Bad TLSA record digest"}, + {DANESSL_R_BAD_NULL_DATA, "Bad TLSA record null data"}, + {DANESSL_R_BAD_PKEY, "Bad TLSA record public key"}, + {DANESSL_R_BAD_SELECTOR, "Bad TLSA record selector"}, + {DANESSL_R_BAD_USAGE, "Bad TLSA record usage"}, + {DANESSL_R_INIT, "DANESSL_init() required"}, + {DANESSL_R_LIBRARY_INIT, "DANESSL_library_init() required"}, + {DANESSL_R_NOSIGN_KEY, "Certificate usage 2 requires EC support"}, + {DANESSL_R_SCTX_INIT, "DANESSL_CTX_init() required"}, + {DANESSL_R_SUPPORT, "DANE library features not supported"}, + {0, NULL} +}; +#endif + +#define DANEerr(f, r) ERR_PUT_error(err_lib_dane, (f), (r), __FUNCTION__, __LINE__) + +static int err_lib_dane = -1; +static int dane_idx = -1; + +#ifdef X509_V_FLAG_PARTIAL_CHAIN /* OpenSSL >= 1.0.2 */ +static int wrap_to_root = 0; +#else +static int wrap_to_root = 1; +#endif + +static void (*cert_free)(void *) = (void (*)(void *)) X509_free; +static void (*pkey_free)(void *) = (void (*)(void *)) EVP_PKEY_free; + +typedef struct dane_list +{ + struct dane_list *next; + void *value; +} *dane_list; + +#define LINSERT(h, e) do { (e)->next = (h); (h) = (e); } while (0) + +typedef struct dane_host_list +{ + struct dane_host_list *next; + char *value; +} *dane_host_list; + +typedef struct dane_data +{ + size_t datalen; + unsigned char data[0]; +} *dane_data; + +typedef struct dane_data_list +{ + struct dane_data_list *next; + dane_data value; +} *dane_data_list; + +typedef struct dane_mtype +{ + int mdlen; + const EVP_MD *md; + dane_data_list data; +} *dane_mtype; + +typedef struct dane_mtype_list +{ + struct dane_mtype_list *next; + dane_mtype value; +} *dane_mtype_list; + +typedef struct dane_selector +{ + uint8_t selector; + dane_mtype_list mtype; +} *dane_selector; + +typedef struct dane_selector_list +{ + struct dane_selector_list *next; + dane_selector value; +} *dane_selector_list; + +typedef struct dane_pkey_list +{ + struct dane_pkey_list *next; + EVP_PKEY *value; +} *dane_pkey_list; + +typedef struct dane_cert_list +{ + struct dane_cert_list *next; + X509 *value; +} *dane_cert_list; + +typedef struct ssl_dane +{ + int (*verify)(X509_STORE_CTX *); + STACK_OF(X509) *roots; + STACK_OF(X509) *chain; + X509 *match; /* Matched cert */ + const char *thost; /* TLSA base domain */ + char *mhost; /* Matched peer name */ + dane_pkey_list pkeys; + dane_cert_list certs; + dane_host_list hosts; + dane_selector_list selectors[DANESSL_USAGE_LAST + 1]; + int depth; + int mdpth; /* Depth of matched cert */ + int multi; /* Multi-label wildcards? */ + int count; /* Number of TLSA records */ +} ssl_dane; + +#ifndef X509_V_ERR_HOSTNAME_MISMATCH +# define X509_V_ERR_HOSTNAME_MISMATCH X509_V_ERR_APPLICATION_VERIFICATION +#endif + + + +static int +match(dane_selector_list slist, X509 *cert, int depth) +{ +int matched; + +/* + * Note, set_trust_anchor() needs to know whether the match was for a + * pkey digest or a certificate digest. We return MATCHED_PKEY or + * MATCHED_CERT accordingly. + */ +#define MATCHED_CERT (DANESSL_SELECTOR_CERT + 1) +#define MATCHED_PKEY (DANESSL_SELECTOR_SPKI + 1) + +/* + * Loop over each selector, mtype, and associated data element looking + * for a match. + */ +for (matched = 0; !matched && slist; slist = slist->next) + { + unsigned char mdbuf[EVP_MAX_MD_SIZE]; + unsigned char *buf = NULL; + unsigned char *buf2; + unsigned int len = 0; + + /* + * Extract ASN.1 DER form of certificate or public key. + */ + switch(slist->value->selector) + { + case DANESSL_SELECTOR_CERT: + len = i2d_X509(cert, NULL); + buf2 = buf = US OPENSSL_malloc(len); + if(buf) i2d_X509(cert, &buf2); + break; + case DANESSL_SELECTOR_SPKI: + len = i2d_X509_PUBKEY(X509_get_X509_PUBKEY(cert), NULL); + buf2 = buf = US OPENSSL_malloc(len); + if(buf) i2d_X509_PUBKEY(X509_get_X509_PUBKEY(cert), &buf2); + break; + } + + if (!buf) + { + DANEerr(DANESSL_F_MATCH, ERR_R_MALLOC_FAILURE); + return 0; + } + OPENSSL_assert(buf2 - buf == len); + + /* + * Loop over each mtype and data element + */ + for (dane_mtype_list m = slist->value->mtype; !matched && m; m = m->next) + { + unsigned char *cmpbuf = buf; + unsigned int cmplen = len; + + /* + * If it is a digest, compute the corresponding digest of the + * DER data for comparison, otherwise, use the full object. + */ + if (m->value->md) + { + cmpbuf = mdbuf; + if (!EVP_Digest(buf, len, cmpbuf, &cmplen, m->value->md, 0)) + matched = -1; + } + for (dane_data_list d = m->value->data; !matched && d; d = d->next) + if ( cmplen == d->value->datalen + && memcmp(cmpbuf, d->value->data, cmplen) == 0) + matched = slist->value->selector + 1; + } + + OPENSSL_free(buf); + } + +return matched; +} + +static int +push_ext(X509 *cert, X509_EXTENSION *ext) +{ +if (ext) + { + if (X509_add_ext(cert, ext, -1)) + return 1; + X509_EXTENSION_free(ext); + } +DANEerr(DANESSL_F_PUSH_EXT, ERR_R_MALLOC_FAILURE); +return 0; +} + +static int +add_ext(X509 *issuer, X509 *subject, int ext_nid, char *ext_val) +{ +X509V3_CTX v3ctx; + +X509V3_set_ctx(&v3ctx, issuer, subject, 0, 0, 0); +return push_ext(subject, X509V3_EXT_conf_nid(0, &v3ctx, ext_nid, ext_val)); +} + +static int +set_serial(X509 *cert, AUTHORITY_KEYID *akid, X509 *subject) +{ +int ret = 0; +BIGNUM *bn; + +if (akid && akid->serial) + return (X509_set_serialNumber(cert, akid->serial)); + +/* + * Add one to subject's serial to avoid collisions between TA serial and + * serial of signing root. + */ +if ( (bn = ASN1_INTEGER_to_BN(X509_get_serialNumber(subject), 0)) != 0 + && BN_add_word(bn, 1) + && BN_to_ASN1_INTEGER(bn, X509_get_serialNumber(cert))) + ret = 1; + +if (bn) + BN_free(bn); +return ret; +} + +static int +add_akid(X509 *cert, AUTHORITY_KEYID *akid) +{ +int nid = NID_authority_key_identifier; +ASN1_OCTET_STRING *id; +unsigned char c = 0; +int ret = 0; + +/* + * 0 will never be our subject keyid from a SHA-1 hash, but it could be + * our subject keyid if forced from child's akid. If so, set our + * authority keyid to 1. This way we are never self-signed, and thus + * exempt from any potential (off by default for now in OpenSSL) + * self-signature checks! + */ +id = akid && akid->keyid ? akid->keyid : 0; +if (id && ASN1_STRING_length(id) == 1 && *ASN1_STRING_get0_data(id) == c) + c = 1; + +if ( (akid = AUTHORITY_KEYID_new()) != 0 + && (akid->keyid = ASN1_OCTET_STRING_new()) != 0 +#ifdef EXIM_HAVE_ASN1_MACROS + && ASN1_OCTET_STRING_set(akid->keyid, (void *) &c, 1) +#else + && M_ASN1_OCTET_STRING_set(akid->keyid, (void *) &c, 1) +#endif + && X509_add1_ext_i2d(cert, nid, akid, 0, X509V3_ADD_APPEND)) + ret = 1; +if (akid) + AUTHORITY_KEYID_free(akid); +return ret; +} + +static int +add_skid(X509 *cert, AUTHORITY_KEYID *akid) +{ +int nid = NID_subject_key_identifier; + +if (!akid || !akid->keyid) + return add_ext(0, cert, nid, "hash"); +return X509_add1_ext_i2d(cert, nid, akid->keyid, 0, X509V3_ADD_APPEND) > 0; +} + +static X509_NAME * +akid_issuer_name(AUTHORITY_KEYID *akid) +{ +if (akid && akid->issuer) + { + GENERAL_NAMES *gens = akid->issuer; + + for (int i = 0; i < sk_GENERAL_NAME_num(gens); ++i) + { + GENERAL_NAME *gn = sk_GENERAL_NAME_value(gens, i); + + if (gn->type == GEN_DIRNAME) + return (gn->d.dirn); + } + } +return 0; +} + +static int +set_issuer_name(X509 *cert, AUTHORITY_KEYID *akid, X509_NAME *subj) +{ +X509_NAME *name = akid_issuer_name(akid); + +/* + * If subject's akid specifies an authority key identifier issuer name, we + * must use that. + */ +return X509_set_issuer_name(cert, + name ? name : subj); +} + +static int +grow_chain(ssl_dane *dane, int trusted, X509 *cert) +{ +STACK_OF(X509) **xs = trusted ? &dane->roots : &dane->chain; +static ASN1_OBJECT *serverAuth = 0; + +#define UNTRUSTED 0 +#define TRUSTED 1 + +if ( trusted && !serverAuth + && !(serverAuth = OBJ_nid2obj(NID_server_auth))) + { + DANEerr(DANESSL_F_GROW_CHAIN, ERR_R_MALLOC_FAILURE); + return 0; + } +if (!*xs && !(*xs = sk_X509_new_null())) + { + DANEerr(DANESSL_F_GROW_CHAIN, ERR_R_MALLOC_FAILURE); + return 0; + } + +if (cert) + { + if (trusted && !X509_add1_trust_object(cert, serverAuth)) + return 0; +#ifdef EXIM_OPAQUE_X509 + X509_up_ref(cert); +#else + CRYPTO_add(&cert->references, 1, CRYPTO_LOCK_X509); +#endif + if (!sk_X509_push(*xs, cert)) + { + X509_free(cert); + DANEerr(DANESSL_F_GROW_CHAIN, ERR_R_MALLOC_FAILURE); + return 0; + } + } +return 1; +} + +static int +wrap_issuer(ssl_dane *dane, EVP_PKEY *key, X509 *subject, int depth, int top) +{ +int ret = 1; +X509 *cert = 0; +AUTHORITY_KEYID *akid; +X509_NAME *name = X509_get_issuer_name(subject); +EVP_PKEY *newkey = key ? key : X509_get_pubkey(subject); + +#define WRAP_MID 0 /* Ensure intermediate. */ +#define WRAP_TOP 1 /* Ensure self-signed. */ + +if (!name || !newkey || !(cert = X509_new())) + return 0; + +/* + * Record the depth of the trust-anchor certificate. + */ +if (dane->depth < 0) + dane->depth = depth + 1; + +/* + * XXX: Uncaught error condition: + * + * The return value is NULL both when the extension is missing, and when + * OpenSSL rans out of memory while parsing the extension. + */ +ERR_clear_error(); +akid = X509_get_ext_d2i(subject, NID_authority_key_identifier, 0, 0); +/* XXX: Should we peek at the error stack here??? */ + +/* + * If top is true generate a self-issued root CA, otherwise an + * intermediate CA and possibly its self-signed issuer. + * + * CA cert valid for +/- 30 days + */ +if ( !X509_set_version(cert, 2) + || !set_serial(cert, akid, subject) + || !set_issuer_name(cert, akid, name) + || !X509_gmtime_adj(X509_getm_notBefore(cert), -30 * 86400L) + || !X509_gmtime_adj(X509_getm_notAfter(cert), 30 * 86400L) + || !X509_set_subject_name(cert, name) + || !X509_set_pubkey(cert, newkey) + || !add_ext(0, cert, NID_basic_constraints, "CA:TRUE") + || (!top && !add_akid(cert, akid)) + || !add_skid(cert, akid) + || ( !top && wrap_to_root + && !wrap_issuer(dane, newkey, cert, depth, WRAP_TOP))) + ret = 0; + +if (akid) + AUTHORITY_KEYID_free(akid); +if (!key) + EVP_PKEY_free(newkey); +if (ret) + ret = grow_chain(dane, !top && wrap_to_root ? UNTRUSTED : TRUSTED, cert); +if (cert) + X509_free(cert); +return ret; +} + +static int +wrap_cert(ssl_dane *dane, X509 *tacert, int depth) +{ +if (dane->depth < 0) + dane->depth = depth + 1; + +/* + * If the TA certificate is self-issued, or need not be, use it directly. + * Otherwise, synthesize requisite ancestors. + */ +if ( !wrap_to_root + || X509_check_issued(tacert, tacert) == X509_V_OK) + return grow_chain(dane, TRUSTED, tacert); + +if (wrap_issuer(dane, 0, tacert, depth, WRAP_MID)) + return grow_chain(dane, UNTRUSTED, tacert); +return 0; +} + +static int +ta_signed(ssl_dane *dane, X509 *cert, int depth) +{ +EVP_PKEY *pk; +int done = 0; + +/* + * First check whether issued and signed by a TA cert, this is cheaper + * than the bare-public key checks below, since we can determine whether + * the candidate TA certificate issued the certificate to be checked + * first (name comparisons), before we bother with signature checks + * (public key operations). + */ +for (dane_cert_list x = dane->certs; !done && x; x = x->next) + { + if (X509_check_issued(x->value, cert) == X509_V_OK) + { + if (!(pk = X509_get_pubkey(x->value))) + { + /* + * The cert originally contained a valid pkey, which does + * not just vanish, so this is most likely a memory error. + */ + done = -1; + break; + } + /* Check signature, since some other TA may work if not this. */ + if (X509_verify(cert, pk) > 0) + done = wrap_cert(dane, x->value, depth) ? 1 : -1; + EVP_PKEY_free(pk); + } + } + +/* + * With bare TA public keys, we can't check whether the trust chain is + * issued by the key, but we can determine whether it is signed by the + * key, so we go with that. + * + * Ideally, the corresponding certificate was presented in the chain, and we + * matched it by its public key digest one level up. This code is here + * to handle adverse conditions imposed by sloppy administrators of + * receiving systems with poorly constructed chains. + * + * We'd like to optimize out keys that should not match when the cert's + * authority key id does not match the key id of this key computed via + * the RFC keyid algorithm (SHA-1 digest of public key bit-string sans + * ASN1 tag and length thus also excluding the unused bits field that is + * logically part of the length). However, some CAs have a non-standard + * authority keyid, so we lose. Too bad. + * + * This may push errors onto the stack when the certificate signature is + * not of the right type or length, throw these away, + */ +for (dane_pkey_list k = dane->pkeys; !done && k; k = k->next) + if (X509_verify(cert, k->value) > 0) + done = wrap_issuer(dane, k->value, cert, depth, WRAP_MID) ? 1 : -1; + else + ERR_clear_error(); + +return done; +} + +static int +set_trust_anchor(X509_STORE_CTX *ctx, ssl_dane *dane, X509 *cert) +{ +int matched = 0; +int depth = 0; +EVP_PKEY *takey; +X509 *ca; +STACK_OF(X509) *in = X509_STORE_CTX_get0_untrusted(ctx); + +if (!grow_chain(dane, UNTRUSTED, 0)) + return -1; + +/* + * Accept a degenerate case: depth 0 self-signed trust-anchor. + */ +if (X509_check_issued(cert, cert) == X509_V_OK) + { + dane->depth = 0; + matched = match(dane->selectors[DANESSL_USAGE_DANE_TA], cert, 0); + if (matched > 0 && !grow_chain(dane, TRUSTED, cert)) + matched = -1; + return matched; + } + +/* Make a shallow copy of the input untrusted chain. */ +if (!(in = sk_X509_dup(in))) + { + DANEerr(DANESSL_F_SET_TRUST_ANCHOR, ERR_R_MALLOC_FAILURE); + return -1; + } + +/* + * At each iteration we consume the issuer of the current cert. This + * reduces the length of the "in" chain by one. If no issuer is found, + * we are done. We also stop when a certificate matches a TA in the + * peer's TLSA RRset. + * + * Caller ensures that the initial certificate is not self-signed. + */ +for (int n = sk_X509_num(in); n > 0; --n, ++depth) + { + int i; + for (i = 0; i < n; ++i) + if (X509_check_issued(sk_X509_value(in, i), cert) == X509_V_OK) + break; + + /* + * Final untrusted element with no issuer in the peer's chain, it may + * however be signed by a pkey or cert obtained via a TLSA RR. + */ + if (i == n) + break; + + /* Peer's chain contains an issuer ca. */ + ca = sk_X509_delete(in, i); + + /* If not a trust anchor, record untrusted ca and continue. */ + if ((matched = match(dane->selectors[DANESSL_USAGE_DANE_TA], ca, + depth + 1)) == 0) + { + if (grow_chain(dane, UNTRUSTED, ca)) + { + if (X509_check_issued(ca, ca) != X509_V_OK) + { + /* Restart with issuer as subject */ + cert = ca; + continue; + } + /* Final self-signed element, skip ta_signed() check. */ + cert = 0; + } + else + matched = -1; + } + else if(matched == MATCHED_CERT) + { + if(!wrap_cert(dane, ca, depth)) + matched = -1; + } + else if(matched == MATCHED_PKEY) + { + if ( !(takey = X509_get_pubkey(ca)) + || !wrap_issuer(dane, takey, cert, depth, WRAP_MID)) + { + if (takey) + EVP_PKEY_free(takey); + else + DANEerr(DANESSL_F_SET_TRUST_ANCHOR, ERR_R_MALLOC_FAILURE); + matched = -1; + } + } + break; + } + +/* Shallow free the duplicated input untrusted chain. */ +sk_X509_free(in); + +/* + * When the loop exits, if "cert" is set, it is not self-signed and has + * no issuer in the chain, we check for a possible signature via a DNS + * obtained TA cert or public key. + */ +if (matched == 0 && cert) + matched = ta_signed(dane, cert, depth); + +return matched; +} + +static int +check_end_entity(X509_STORE_CTX *ctx, ssl_dane *dane, X509 *cert) +{ +int matched; + +matched = match(dane->selectors[DANESSL_USAGE_DANE_EE], cert, 0); +if (matched > 0) + { + dane->mdpth = 0; + dane->match = cert; + X509_up_ref(cert); + if(!X509_STORE_CTX_get0_chain(ctx)) + { + STACK_OF(X509) * sk = sk_X509_new_null(); + if (sk && sk_X509_push(sk, cert)) + { + X509_up_ref(cert); + X509_STORE_CTX_set0_verified_chain(ctx, sk); + } + else + { + if (sk) sk_X509_free(sk); + DANEerr(DANESSL_F_CHECK_END_ENTITY, ERR_R_MALLOC_FAILURE); + return -1; + } + } + } +return matched; +} + +static int +match_name(const char *certid, ssl_dane *dane) +{ +int multi = dane->multi; + +for (dane_host_list hosts = dane->hosts; hosts; hosts = hosts->next) + { + int match_subdomain = 0; + const char *domain = hosts->value; + const char *parent; + int idlen; + int domlen; + + if (*domain == '.' && domain[1] != '\0') + { + ++domain; + match_subdomain = 1; + } + + /* + * Sub-domain match: certid is any sub-domain of hostname. + */ + if(match_subdomain) + { + if ( (idlen = strlen(certid)) > (domlen = strlen(domain)) + 1 + && certid[idlen - domlen - 1] == '.' + && !strcasecmp(certid + (idlen - domlen), domain)) + return 1; + else + continue; + } + + /* + * Exact match and initial "*" match. The initial "*" in a certid + * matches one (if multi is false) or more hostname components under + * the condition that the certid contains multiple hostname components. + */ + if ( !strcasecmp(certid, domain) + || ( certid[0] == '*' && certid[1] == '.' && certid[2] != 0 + && (parent = strchr(domain, '.')) != 0 + && (idlen = strlen(certid + 1)) <= (domlen = strlen(parent)) + && strcasecmp(multi ? parent + domlen - idlen : parent, certid+1) == 0)) + return 1; + } +return 0; +} + +static const char * +check_name(const char *name, int len) +{ +const char *cp = name + len; + +while (len > 0 && !*--cp) + --len; /* Ignore trailing NULs */ +if (len <= 0) + return 0; +for (cp = name; *cp; cp++) + { + char c = *cp; + if (!((c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + (c >= 'A' && c <= 'Z') || + (c == '.' || c == '-') || + (c == '*'))) + return 0; /* Only LDH, '.' and '*' */ + } +if (cp - name != len) /* Guard against internal NULs */ + return 0; +return name; +} + +static const char * +parse_dns_name(const GENERAL_NAME *gn) +{ +if (gn->type != GEN_DNS) + return 0; +if (ASN1_STRING_type(gn->d.ia5) != V_ASN1_IA5STRING) + return 0; +return check_name(CCS ASN1_STRING_get0_data(gn->d.ia5), + ASN1_STRING_length(gn->d.ia5)); +} + +static char * +parse_subject_name(X509 *cert) +{ +X509_NAME *name = X509_get_subject_name(cert); +X509_NAME_ENTRY *entry; +ASN1_STRING *entry_str; +unsigned char *namebuf; +int nid = NID_commonName; +int len; +int i; + +if (!name || (i = X509_NAME_get_index_by_NID(name, nid, -1)) < 0) + return 0; +if (!(entry = X509_NAME_get_entry(name, i))) + return 0; +if (!(entry_str = X509_NAME_ENTRY_get_data(entry))) + return 0; + +if ((len = ASN1_STRING_to_UTF8(&namebuf, entry_str)) < 0) + return 0; +if (len <= 0 || check_name(CS namebuf, len) == 0) + { + OPENSSL_free(namebuf); + return 0; + } +return CS namebuf; +} + +static int +name_check(ssl_dane *dane, X509 *cert) +{ +int matched = 0; +BOOL got_altname = FALSE; +GENERAL_NAMES *gens; + +gens = X509_get_ext_d2i(cert, NID_subject_alt_name, 0, 0); +if (gens) + { + int n = sk_GENERAL_NAME_num(gens); + + for (int i = 0; i < n; ++i) + { + const GENERAL_NAME *gn = sk_GENERAL_NAME_value(gens, i); + const char *certid; + + if (gn->type != GEN_DNS) + continue; + got_altname = TRUE; + certid = parse_dns_name(gn); + if (certid && *certid) + { + if ((matched = match_name(certid, dane)) == 0) + continue; + if (!(dane->mhost = OPENSSL_strdup(certid))) + matched = -1; + DEBUG(D_tls) debug_printf("Dane name_check: matched SAN %s\n", certid); + break; + } + } + GENERAL_NAMES_free(gens); + } + +/* + * XXX: Should the subjectName be skipped when *any* altnames are present, + * or only when DNS altnames are present? + */ +if (!got_altname) + { + char *certid = parse_subject_name(cert); + if (certid != 0 && *certid && (matched = match_name(certid, dane)) != 0) + { + DEBUG(D_tls) debug_printf("Dane name_check: matched SN %s\n", certid); + dane->mhost = OPENSSL_strdup(certid); + } + if (certid) + OPENSSL_free(certid); + } +return matched; +} + +static int +verify_chain(X509_STORE_CTX *ctx) +{ +int (*cb)(int, X509_STORE_CTX *) = X509_STORE_CTX_get_verify_cb(ctx); +X509 *cert = X509_STORE_CTX_get0_cert(ctx); +STACK_OF(X509) * chain = X509_STORE_CTX_get0_chain(ctx); +int chain_length = sk_X509_num(chain); +int ssl_idx = SSL_get_ex_data_X509_STORE_CTX_idx(); +SSL *ssl = X509_STORE_CTX_get_ex_data(ctx, ssl_idx); +ssl_dane *dane = SSL_get_ex_data(ssl, dane_idx); +dane_selector_list issuer_rrs = dane->selectors[DANESSL_USAGE_PKIX_TA]; +dane_selector_list leaf_rrs = dane->selectors[DANESSL_USAGE_PKIX_EE]; +int matched = 0; + +DEBUG(D_tls) debug_printf("Dane verify_chain\n"); + +/* Restore OpenSSL's internal_verify() as the signature check function */ +X509_STORE_CTX_set_verify(ctx, dane->verify); + +if ((matched = name_check(dane, cert)) < 0) + { + X509_STORE_CTX_set_error(ctx, X509_V_ERR_OUT_OF_MEM); + return 0; + } + +if (!matched) + { + X509_STORE_CTX_set_error_depth(ctx, 0); + X509_STORE_CTX_set_current_cert(ctx, cert); + X509_STORE_CTX_set_error(ctx, X509_V_ERR_HOSTNAME_MISMATCH); + if (!cb(0, ctx)) + return 0; + } +matched = 0; + +/* + * Satisfy at least one usage 0 or 1 constraint, unless we've already + * matched a usage 2 trust anchor. + * + * XXX: internal_verify() doesn't callback with top certs that are not + * self-issued. This is fixed in OpenSSL 1.1.0. + */ +if (dane->roots && sk_X509_num(dane->roots)) + { + X509 *top = sk_X509_value(chain, dane->depth); + + dane->mdpth = dane->depth; + dane->match = top; + X509_up_ref(top); + +#if OPENSSL_VERSION_NUMBER < 0x10100000L + if (X509_check_issued(top, top) != X509_V_OK) + { + X509_STORE_CTX_set_error_depth(ctx, dane->depth); + X509_STORE_CTX_set_current_cert(ctx, top); + if (!cb(1, ctx)) + return 0; + } +#endif + /* Pop synthetic trust-anchor ancestors off the chain! */ + while (--chain_length > dane->depth) + X509_free(sk_X509_pop(chain)); + } +else + { + int n = 0; + X509 *xn = cert; + + /* + * Check for an EE match, then a CA match at depths > 0, and + * finally, if the EE cert is self-issued, for a depth 0 CA match. + */ + if (leaf_rrs) + matched = match(leaf_rrs, xn, 0); + if (matched) DEBUG(D_tls) debug_printf("Dane verify_chain: matched EE\n"); + + if (!matched && issuer_rrs) + for (n = chain_length-1; !matched && n >= 0; --n) + { + xn = sk_X509_value(chain, n); + if (n > 0 || X509_check_issued(xn, xn) == X509_V_OK) + matched = match(issuer_rrs, xn, n); + } + if (matched) DEBUG(D_tls) debug_printf("Dane verify_chain: matched %s\n", + n>0 ? "CA" : "selfisssued EE"); + + if (!matched) + { + X509_STORE_CTX_set_error_depth(ctx, 0); + X509_STORE_CTX_set_current_cert(ctx, cert); + X509_STORE_CTX_set_error(ctx, X509_V_ERR_CERT_UNTRUSTED); + if (!cb(0, ctx)) + return 0; + } + else + { + dane->mdpth = n; + dane->match = xn; + X509_up_ref(xn); + } + } + +/* Tail recurse into OpenSSL's internal_verify */ +return dane->verify(ctx); +} + +static void +dane_reset(ssl_dane *dane) +{ +dane->depth = -1; +if (dane->mhost) + { + OPENSSL_free(dane->mhost); + dane->mhost = 0; + } +if (dane->roots) + { + sk_X509_pop_free(dane->roots, X509_free); + dane->roots = 0; + } +if (dane->chain) + { + sk_X509_pop_free(dane->chain, X509_free); + dane->chain = 0; + } +if (dane->match) + { + X509_free(dane->match); + dane->match = 0; + } +dane->mdpth = -1; +} + +static int +verify_cert(X509_STORE_CTX *ctx, void *unused_ctx) +{ +static int ssl_idx = -1; +SSL *ssl; +ssl_dane *dane; +int (*cb)(int, X509_STORE_CTX *) = X509_STORE_CTX_get_verify_cb(ctx); +X509 *cert = X509_STORE_CTX_get0_cert(ctx); +int matched; + +DEBUG(D_tls) debug_printf("Dane verify_cert\n"); + +if (ssl_idx < 0) + ssl_idx = SSL_get_ex_data_X509_STORE_CTX_idx(); +if (dane_idx < 0) + { + DANEerr(DANESSL_F_VERIFY_CERT, ERR_R_MALLOC_FAILURE); + return -1; + } + +ssl = X509_STORE_CTX_get_ex_data(ctx, ssl_idx); +if (!(dane = SSL_get_ex_data(ssl, dane_idx)) || !cert) + return X509_verify_cert(ctx); + +/* Reset for verification of a new chain, perhaps a renegotiation. */ +dane_reset(dane); + +if (dane->selectors[DANESSL_USAGE_DANE_EE]) + { + if ((matched = check_end_entity(ctx, dane, cert)) > 0) + { + X509_STORE_CTX_set_error_depth(ctx, 0); + X509_STORE_CTX_set_current_cert(ctx, cert); + return cb(1, ctx); + } + if (matched < 0) + { + X509_STORE_CTX_set_error(ctx, X509_V_ERR_OUT_OF_MEM); + return -1; + } + } + +if (dane->selectors[DANESSL_USAGE_DANE_TA]) + { + if ((matched = set_trust_anchor(ctx, dane, cert)) < 0) + { + X509_STORE_CTX_set_error(ctx, X509_V_ERR_OUT_OF_MEM); + return -1; + } + if (matched) + { + /* + * Check that setting the untrusted chain updates the expected + * structure member at the expected offset. + */ + X509_STORE_CTX_trusted_stack(ctx, dane->roots); + X509_STORE_CTX_set_chain(ctx, dane->chain); + OPENSSL_assert(dane->chain == X509_STORE_CTX_get0_untrusted(ctx)); + } + } + +/* + * Name checks and usage 0/1 constraint enforcement are delayed until + * X509_verify_cert() builds the full chain and calls our verify_chain() + * wrapper. + */ +dane->verify = X509_STORE_CTX_get_verify(ctx); +X509_STORE_CTX_set_verify(ctx, verify_chain); + +if (X509_verify_cert(ctx)) + return 1; + +/* + * If the chain is invalid, clear any matching cert or hostname, to + * protect callers that might erroneously rely on these alone without + * checking the validation status. + */ +if (dane->match) + { + X509_free(dane->match); + dane->match = 0; + } +if (dane->mhost) + { + OPENSSL_free(dane->mhost); + dane->mhost = 0; + } + return 0; +} + +static dane_list +list_alloc(size_t vsize) +{ +void *value = (void *) OPENSSL_malloc(vsize); +dane_list l; + +if (!value) + { + DANEerr(DANESSL_F_LIST_ALLOC, ERR_R_MALLOC_FAILURE); + return 0; + } +if (!(l = (dane_list) OPENSSL_malloc(sizeof(*l)))) + { + OPENSSL_free(value); + DANEerr(DANESSL_F_LIST_ALLOC, ERR_R_MALLOC_FAILURE); + return 0; + } +l->next = 0; +l->value = value; +return l; +} + +static void +list_free(void *list, void (*f)(void *)) +{ +dane_list next; + +for (dane_list head = (dane_list) list; head; head = next) + { + next = head->next; + if (f && head->value) + f(head->value); + OPENSSL_free(head); + } +} + +static void +ossl_free(void * p) +{ +OPENSSL_free(p); +} + +static void +dane_mtype_free(void *p) +{ +list_free(((dane_mtype) p)->data, ossl_free); +OPENSSL_free(p); +} + +static void +dane_selector_free(void *p) +{ +list_free(((dane_selector) p)->mtype, dane_mtype_free); +OPENSSL_free(p); +} + + + +/* + +Tidy up once the connection is finished with. + +Arguments + ssl The ssl connection handle + +=> Before calling SSL_free() +tls_close() and tls_getc() [the error path] are the obvious places. +Could we do it earlier - right after verification? In tls_client_start() +right after SSL_connect() returns, in that case. + +*/ + +void +DANESSL_cleanup(SSL *ssl) +{ +ssl_dane *dane; + +DEBUG(D_tls) debug_printf("Dane lib-cleanup\n"); + +if (dane_idx < 0 || !(dane = SSL_get_ex_data(ssl, dane_idx))) + return; +(void) SSL_set_ex_data(ssl, dane_idx, 0); + +dane_reset(dane); +if (dane->hosts) + list_free(dane->hosts, ossl_free); +for (int u = 0; u <= DANESSL_USAGE_LAST; ++u) + if (dane->selectors[u]) + list_free(dane->selectors[u], dane_selector_free); +if (dane->pkeys) + list_free(dane->pkeys, pkey_free); +if (dane->certs) + list_free(dane->certs, cert_free); +OPENSSL_free(dane); +} + +static dane_host_list +host_list_init(const char **src) +{ +dane_host_list head = NULL; + +while (*src) + { + dane_host_list elem = (dane_host_list) OPENSSL_malloc(sizeof(*elem)); + if (elem == 0) + { + list_free(head, ossl_free); + return 0; + } + elem->value = OPENSSL_strdup(*src++); + LINSERT(head, elem); + } +return head; +} + + +int +DANESSL_get_match_cert(SSL *ssl, X509 **match, const char **mhost, int *depth) +{ +ssl_dane *dane; + +if (dane_idx < 0 || (dane = SSL_get_ex_data(ssl, dane_idx)) == 0) + { + DANEerr(DANESSL_F_ADD_TLSA, DANESSL_R_INIT); + return -1; + } + +if (dane->match) + { + if (match) + *match = dane->match; + if (mhost) + *mhost = dane->mhost; + if (depth) + *depth = dane->mdpth; + } + + return (dane->match != 0); +} + + +#ifdef never_called +int +DANESSL_verify_chain(SSL *ssl, STACK_OF(X509) *chain) +{ +int ret; +X509 *cert; +X509_STORE_CTX * store_ctx; +SSL_CTX *ssl_ctx = SSL_get_SSL_CTX(ssl); +X509_STORE *store = SSL_CTX_get_cert_store(ssl_ctx); +int store_ctx_idx = SSL_get_ex_data_X509_STORE_CTX_idx(); + +cert = sk_X509_value(chain, 0); +if (!(store_ctx = X509_STORE_CTX_new())) + { + DANEerr(DANESSL_F_DANESSL_VERIFY_CHAIN, ERR_R_MALLOC_FAILURE); + return 0; + } +if (!X509_STORE_CTX_init(store_ctx, store, cert, chain)) + { + X509_STORE_CTX_free(store_ctx); + return 0; + } +X509_STORE_CTX_set_ex_data(store_ctx, store_ctx_idx, ssl); + +X509_STORE_CTX_set_default(store_ctx, + SSL_is_server(ssl) ? "ssl_client" : "ssl_server"); +X509_VERIFY_PARAM_set1(X509_STORE_CTX_get0_param(store_ctx), + SSL_get0_param(ssl)); + +if (SSL_get_verify_callback(ssl)) + X509_STORE_CTX_set_verify_cb(store_ctx, SSL_get_verify_callback(ssl)); + +ret = verify_cert(store_ctx, NULL); + +SSL_set_verify_result(ssl, X509_STORE_CTX_get_error(store_ctx)); +X509_STORE_CTX_cleanup(store_ctx); + +return (ret); +} +#endif + + + + +/* + +Call this for each TLSA record found for the target, after the +DANE setup has been done on the ssl connection handle. + +Arguments: + ssl Connection handle + usage TLSA record field + selector TLSA record field + mdname ??? message digest name? + data ??? TLSA record megalump? + dlen length of data + +Return + -1 on error + 0 action not taken + 1 record accepted +*/ + +int +DANESSL_add_tlsa(SSL *ssl, uint8_t usage, uint8_t selector, const char *mdname, + unsigned const char *data, size_t dlen) +{ +ssl_dane *dane; +dane_selector_list s = 0; +dane_mtype_list m = 0; +dane_data_list d = 0; +dane_cert_list xlist = 0; +dane_pkey_list klist = 0; +const EVP_MD *md = 0; + +DEBUG(D_tls) debug_printf("Dane add-tlsa: usage %u sel %u mdname \"%s\"\n", + usage, selector, mdname); + +if(dane_idx < 0 || !(dane = SSL_get_ex_data(ssl, dane_idx))) + { + DANEerr(DANESSL_F_ADD_TLSA, DANESSL_R_INIT); + return -1; + } + +if (usage > DANESSL_USAGE_LAST) + { + DANEerr(DANESSL_F_ADD_TLSA, DANESSL_R_BAD_USAGE); + return 0; + } +if (selector > DANESSL_SELECTOR_LAST) + { + DANEerr(DANESSL_F_ADD_TLSA, DANESSL_R_BAD_SELECTOR); + return 0; + } + +/* Support built-in standard one-digit mtypes */ +if (mdname && *mdname && mdname[1] == '\0') + switch (*mdname - '0') + { + case DANESSL_MATCHING_FULL: mdname = 0; break; + case DANESSL_MATCHING_2256: mdname = "sha256"; break; + case DANESSL_MATCHING_2512: mdname = "sha512"; break; + } +if (mdname && *mdname && !(md = EVP_get_digestbyname(mdname))) + { + DANEerr(DANESSL_F_ADD_TLSA, DANESSL_R_BAD_DIGEST); + return 0; + } +if (mdname && *mdname && dlen != EVP_MD_size(md)) + { + DANEerr(DANESSL_F_ADD_TLSA, DANESSL_R_BAD_DATA_LENGTH); + return 0; + } +if (!data) + { + DANEerr(DANESSL_F_ADD_TLSA, DANESSL_R_BAD_NULL_DATA); + return 0; + } + +/* + * Full Certificate or Public Key when NULL or empty digest name + */ +if (!mdname || !*mdname) + { + X509 *x = 0; + EVP_PKEY *k = 0; + const unsigned char *p = data; + +#define xklistinit(lvar, ltype, var, freeFunc) do { \ + (lvar) = (ltype) OPENSSL_malloc(sizeof(*(lvar))); \ + if ((lvar) == 0) { \ + DANEerr(DANESSL_F_ADD_TLSA, ERR_R_MALLOC_FAILURE); \ + freeFunc((var)); \ + return 0; \ + } \ + (lvar)->next = 0; \ + lvar->value = var; \ + } while (0) +#define xkfreeret(ret) do { \ + if (xlist) list_free(xlist, cert_free); \ + if (klist) list_free(klist, pkey_free); \ + return (ret); \ + } while (0) + + switch (selector) + { + case DANESSL_SELECTOR_CERT: + if (!d2i_X509(&x, &p, dlen) || dlen != p - data) + { + if (x) + X509_free(x); + DANEerr(DANESSL_F_ADD_TLSA, DANESSL_R_BAD_CERT); + return 0; + } + k = X509_get_pubkey(x); + EVP_PKEY_free(k); + if (k == 0) + { + X509_free(x); + DANEerr(DANESSL_F_ADD_TLSA, DANESSL_R_BAD_CERT_PKEY); + return 0; + } + if (usage == DANESSL_USAGE_DANE_TA) + xklistinit(xlist, dane_cert_list, x, X509_free); + break; + + case DANESSL_SELECTOR_SPKI: + if (!d2i_PUBKEY(&k, &p, dlen) || dlen != p - data) + { + if (k) + EVP_PKEY_free(k); + DANEerr(DANESSL_F_ADD_TLSA, DANESSL_R_BAD_PKEY); + return 0; + } + if (usage == DANESSL_USAGE_DANE_TA) + xklistinit(klist, dane_pkey_list, k, EVP_PKEY_free); + break; + } + } + +/* Find insertion point and don't add duplicate elements. */ +for (s = dane->selectors[usage]; s; s = s->next) + if (s->value->selector == selector) + { + for (m = s->value->mtype; m; m = m->next) + if (m->value->md == md) + { + for (d = m->value->data; d; d = d->next) + if ( d->value->datalen == dlen + && memcmp(d->value->data, data, dlen) == 0) + xkfreeret(1); + break; + } + break; + } + +if ((d = (dane_data_list) list_alloc(sizeof(*d->value) + dlen)) == 0) + xkfreeret(0); +d->value->datalen = dlen; +memcpy(d->value->data, data, dlen); +if (!m) + { + if ((m = (dane_mtype_list) list_alloc(sizeof(*m->value))) == 0) + { + list_free(d, ossl_free); + xkfreeret(0); + } + m->value->data = 0; + if ((m->value->md = md) != 0) + m->value->mdlen = dlen; + if (!s) + { + if ((s = (dane_selector_list) list_alloc(sizeof(*s->value))) == 0) + { + list_free(m, dane_mtype_free); + xkfreeret(0); + } + s->value->mtype = 0; + s->value->selector = selector; + LINSERT(dane->selectors[usage], s); + } + LINSERT(s->value->mtype, m); + } +LINSERT(m->value->data, d); + +if (xlist) + LINSERT(dane->certs, xlist); +else if (klist) + LINSERT(dane->pkeys, klist); +++dane->count; +return 1; +} + + + + +/* +Call this once we have an ssl connection handle but before +making the TLS connection. + +=> In tls_client_start() after the call to SSL_new() +and before the call to SSL_connect(). Exactly where +probably does not matter. +We probably want to keep our existing SNI handling; +call this with NULL. + +Arguments: + ssl Connection handle + sni_domain Optional peer server name + hostnames list of names to chack against peer cert + +Return + -1 on fatal error + 0 nonfatal error + 1 success +*/ + +int +DANESSL_init(SSL *ssl, const char *sni_domain, const char **hostnames) +{ +ssl_dane *dane; + +DEBUG(D_tls) debug_printf("Dane ssl_init\n"); +if (dane_idx < 0) + { + DANEerr(DANESSL_F_INIT, DANESSL_R_LIBRARY_INIT); + return -1; + } + +if (sni_domain && !SSL_set_tlsext_host_name(ssl, sni_domain)) + return 0; + +if ((dane = (ssl_dane *) OPENSSL_malloc(sizeof(ssl_dane))) == 0) + { + DANEerr(DANESSL_F_INIT, ERR_R_MALLOC_FAILURE); + return 0; + } +if (!SSL_set_ex_data(ssl, dane_idx, dane)) + { + DANEerr(DANESSL_F_INIT, ERR_R_MALLOC_FAILURE); + OPENSSL_free(dane); + return 0; + } + +dane->verify = 0; +dane->hosts = 0; +dane->thost = 0; +dane->pkeys = 0; +dane->certs = 0; +dane->chain = 0; +dane->match = 0; +dane->roots = 0; +dane->depth = -1; +dane->mhost = 0; /* Future SSL control interface */ +dane->mdpth = 0; /* Future SSL control interface */ +dane->multi = 0; /* Future SSL control interface */ +dane->count = 0; +dane->hosts = 0; + +for (int i = 0; i <= DANESSL_USAGE_LAST; ++i) + dane->selectors[i] = 0; + +if (hostnames && (dane->hosts = host_list_init(hostnames)) == 0) + { + DANEerr(DANESSL_F_INIT, ERR_R_MALLOC_FAILURE); + DANESSL_cleanup(ssl); + return 0; + } + +return 1; +} + + +/* + +Call this once we have a context to work with, but +before DANESSL_init() + +=> in tls_client_start(), after tls_init() call gives us the ctx, +if we decide we want to (policy) and can (TLSA records available) +replacing (? what about fallback) everything from testing tls_verify_hosts +down to just before calling SSL_new() for the conn handle. + +Arguments + ctx SSL context + +Return + -1 Error + 1 Success +*/ + +int +DANESSL_CTX_init(SSL_CTX *ctx) +{ +DEBUG(D_tls) debug_printf("Dane ctx-init\n"); +if (dane_idx >= 0) + { + SSL_CTX_set_cert_verify_callback(ctx, verify_cert, 0); + return 1; + } +DANEerr(DANESSL_F_CTX_INIT, DANESSL_R_LIBRARY_INIT); +return -1; +} + +static void +dane_init(void) +{ +/* + * Store library id in zeroth function slot, used to locate the library + * name. This must be done before we load the error strings. + */ +err_lib_dane = ERR_get_next_error_library(); + +#ifndef OPENSSL_NO_ERR +if (err_lib_dane > 0) + { + dane_str_functs[0].error |= ERR_PACK(err_lib_dane, 0, 0); + ERR_load_strings(err_lib_dane, dane_str_functs); + ERR_load_strings(err_lib_dane, dane_str_reasons); + } +#endif + +/* + * Register SHA-2 digests, if implemented and not already registered. + */ +#if defined(LN_sha256) && defined(NID_sha256) && !defined(OPENSSL_NO_SHA256) +if (!EVP_get_digestbyname(LN_sha224)) EVP_add_digest(EVP_sha224()); +if (!EVP_get_digestbyname(LN_sha256)) EVP_add_digest(EVP_sha256()); +#endif +#if defined(LN_sha512) && defined(NID_sha512) && !defined(OPENSSL_NO_SHA512) +if (!EVP_get_digestbyname(LN_sha384)) EVP_add_digest(EVP_sha384()); +if (!EVP_get_digestbyname(LN_sha512)) EVP_add_digest(EVP_sha512()); +#endif + +/* + * Register an SSL index for the connection-specific ssl_dane structure. + * Using a separate index makes it possible to add DANE support to + * existing OpenSSL releases that don't have a suitable pointer in the + * SSL structure. + */ +dane_idx = SSL_get_ex_new_index(0, 0, 0, 0, 0); +} + + +#if OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER) +static void +run_once(volatile int * once, void (*init)(void)) +{ +int wlock = 0; + +CRYPTO_r_lock(CRYPTO_LOCK_SSL_CTX); +if (!*once) + { + CRYPTO_r_unlock(CRYPTO_LOCK_SSL_CTX); + CRYPTO_w_lock(CRYPTO_LOCK_SSL_CTX); + wlock = 1; + if (!*once) + { + *once = 1; + init(); + } + } +if (wlock) + CRYPTO_w_unlock(CRYPTO_LOCK_SSL_CTX); +else + CRYPTO_r_unlock(CRYPTO_LOCK_SSL_CTX); +} +#endif + + + +/* + +Call this once. Probably early in startup will do; may need +to be after SSL library init. + +=> put after call to tls_init() for now + +Return + 1 Success + 0 Fail +*/ + +int +DANESSL_library_init(void) +{ +static CRYPTO_ONCE once = CRYPTO_ONCE_STATIC_INIT; + +DEBUG(D_tls) debug_printf("Dane lib-init\n"); +(void) CRYPTO_THREAD_run_once(&once, dane_init); + +#if defined(LN_sha256) +/* No DANE without SHA256 support */ +if (dane_idx >= 0 && EVP_get_digestbyname(LN_sha256) != 0) + return 1; +#endif +DANEerr(DANESSL_F_LIBRARY_INIT, DANESSL_R_SUPPORT); +return 0; +} + + +/* vi: aw ai sw=2 +*/ diff --git a/src/dane.c b/src/dane.c new file mode 100644 index 0000000..5ba6196 --- /dev/null +++ b/src/dane.c @@ -0,0 +1,48 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2012, 2014 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* This module provides DANE (RFC6659) support for Exim. See also +the draft RFC for DANE-over-SMTP, "SMTP security via opportunistic DANE TLS" +(V. Dukhovni, W. Hardaker) - version 10, dated May 25, 2014. + +The code for DANE support with Openssl was provided by V.Dukhovni. + +No cryptographic code is included in Exim. All this module does is to call +functions from the OpenSSL or GNU TLS libraries. */ + + +#include "exim.h" + +/* This module is compiled only when it is specifically requested in the +build-time configuration. However, some compilers don't like compiling empty +modules, so keep them happy with a dummy when skipping the rest. Make it +reference itself to stop picky compilers complaining that it is unused, and put +in a dummy argument to stop even pickier compilers complaining about infinite +loops. */ + +#ifndef SUPPORT_DANE +static void dummy(int x) { dummy(x-1); } +#else + +/* Enabling DANE without enabling TLS cannot work. Abort the compilation. */ +# ifdef DISABLE_TLS +# error DANE support requires that TLS support must be enabled. Abort build. +# endif + +/* DNSSEC support is also required */ +# ifndef RES_USE_DNSSEC +# error DANE support requires that the DNS resolver library supports DNSSEC +# endif + +# ifdef USE_OPENSSL +# include "dane-openssl.c" +# endif + + +#endif /* SUPPORT_DANE */ + +/* End of dane.c */ diff --git a/src/danessl.h b/src/danessl.h new file mode 100644 index 0000000..1d6439e --- /dev/null +++ b/src/danessl.h @@ -0,0 +1,47 @@ +/* + * Author: Viktor Dukhovni + * License: THIS CODE IS IN THE PUBLIC DOMAIN. + */ +#ifndef HEADER_DANESSL_H +#define HEADER_DANESSL_H + +#include +#include + +/*- + * Certificate usages: + * https://tools.ietf.org/html/rfc6698#section-2.1.1 + */ +#define DANESSL_USAGE_PKIX_TA 0 +#define DANESSL_USAGE_PKIX_EE 1 +#define DANESSL_USAGE_DANE_TA 2 +#define DANESSL_USAGE_DANE_EE 3 +#define DANESSL_USAGE_LAST DANESSL_USAGE_DANE_EE + +/*- + * Selectors: + * https://tools.ietf.org/html/rfc6698#section-2.1.2 + */ +#define DANESSL_SELECTOR_CERT 0 +#define DANESSL_SELECTOR_SPKI 1 +#define DANESSL_SELECTOR_LAST DANESSL_SELECTOR_SPKI + +/*- + * Matching types: + * https://tools.ietf.org/html/rfc6698#section-2.1.3 + */ +#define DANESSL_MATCHING_FULL 0 +#define DANESSL_MATCHING_2256 1 +#define DANESSL_MATCHING_2512 2 +#define DANESSL_MATCHING_LAST DANESSL_MATCHING_2512 + +extern int DANESSL_library_init(void); +extern int DANESSL_CTX_init(SSL_CTX *); +extern int DANESSL_init(SSL *, const char *, const char **); +extern void DANESSL_cleanup(SSL *); +extern int DANESSL_add_tlsa(SSL *, uint8_t, uint8_t, const char *, + unsigned const char *, size_t); +extern int DANESSL_get_match_cert(SSL *, X509 **, const char **, int *); +extern int DANESSL_verify_chain(SSL *, STACK_OF(X509) *); + +#endif diff --git a/src/dbfn.c b/src/dbfn.c new file mode 100644 index 0000000..ea94b7f --- /dev/null +++ b/src/dbfn.c @@ -0,0 +1,681 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +#include "exim.h" + +/* We have buffers holding path names for database files. +PATH_MAX could be used here, but would be wasting memory, as we deal +with database files like $spooldirectory/db/ */ +#define PATHLEN 256 + + +/* Functions for accessing Exim's hints database, which consists of a number of +different DBM files. This module does not contain code for reading DBM files +for (e.g.) alias expansion. That is all contained within the general search +functions. As Exim now has support for several DBM interfaces, all the relevant +functions are called as macros. + +All the data in Exim's database is in the nature of *hints*. Therefore it +doesn't matter if it gets destroyed by accident. These functions are not +supposed to implement a "safe" database. + +Keys are passed in as C strings, and the terminating zero *is* used when +building the dbm files. This just makes life easier when scanning the files +sequentially. + +Synchronization is required on the database files, and this is achieved by +means of locking on independent lock files. (Earlier attempts to lock on the +DBM files themselves were never completely successful.) Since callers may in +general want to do more than one read or write while holding the lock, there +are separate open and close functions. However, the calling modules should +arrange to hold the locks for the bare minimum of time. */ + + + +/************************************************* +* Open and lock a database file * +*************************************************/ + +/* Used for accessing Exim's hints databases. + +Arguments: + name The single-component name of one of Exim's database files. + flags Either O_RDONLY or O_RDWR, indicating the type of open required; + O_RDWR implies "create if necessary" + dbblock Points to an open_db block to be filled in. + lof If TRUE, write to the log for actual open failures (locking failures + are always logged). + panic If TRUE, panic on failure to create the db directory + +Returns: NULL if the open failed, or the locking failed. After locking + failures, errno is zero. + + On success, dbblock is returned. This contains the dbm pointer and + the fd of the locked lock file. + +There are some calls that use O_RDWR|O_CREAT for the flags. Having discovered +this in December 2005, I'm not sure if this is correct or not, but for the +moment I haven't changed them. +*/ + +open_db * +dbfn_open(uschar *name, int flags, open_db *dbblock, BOOL lof, BOOL panic) +{ +int rc, save_errno; +BOOL read_only = flags == O_RDONLY; +flock_t lock_data; +uschar dirname[PATHLEN], filename[PATHLEN]; + +DEBUG(D_hints_lookup) acl_level++; + +/* The first thing to do is to open a separate file on which to lock. This +ensures that Exim has exclusive use of the database before it even tries to +open it. Early versions tried to lock on the open database itself, but that +gave rise to mysterious problems from time to time - it was suspected that some +DB libraries "do things" on their open() calls which break the interlocking. +The lock file is never written to, but we open it for writing so we can get a +write lock if required. If it does not exist, we create it. This is done +separately so we know when we have done it, because when running as root we +need to change the ownership - see the bottom of this function. We also try to +make the directory as well, just in case. We won't be doing this many times +unnecessarily, because usually the lock file will be there. If the directory +exists, there is no error. */ + +snprintf(CS dirname, sizeof(dirname), "%s/db", spool_directory); +snprintf(CS filename, sizeof(filename), "%s/%s.lockfile", dirname, name); + +priv_drop_temp(exim_uid, exim_gid); +if ((dbblock->lockfd = Uopen(filename, O_RDWR, EXIMDB_LOCKFILE_MODE)) < 0) + { + (void)directory_make(spool_directory, US"db", EXIMDB_DIRECTORY_MODE, panic); + dbblock->lockfd = Uopen(filename, O_RDWR|O_CREAT, EXIMDB_LOCKFILE_MODE); + } +priv_restore(); + +if (dbblock->lockfd < 0) + { + log_write(0, LOG_MAIN, "%s", + string_open_failed("database lock file %s", filename)); + errno = 0; /* Indicates locking failure */ + DEBUG(D_hints_lookup) acl_level--; + return NULL; + } + +/* Now we must get a lock on the opened lock file; do this with a blocking +lock that times out. */ + +lock_data.l_type = read_only? F_RDLCK : F_WRLCK; +lock_data.l_whence = lock_data.l_start = lock_data.l_len = 0; + +DEBUG(D_hints_lookup|D_retry|D_route|D_deliver) + debug_printf_indent("locking %s\n", filename); + +sigalrm_seen = FALSE; +ALARM(EXIMDB_LOCK_TIMEOUT); +rc = fcntl(dbblock->lockfd, F_SETLKW, &lock_data); +ALARM_CLR(0); + +if (sigalrm_seen) errno = ETIMEDOUT; +if (rc < 0) + { + log_write(0, LOG_MAIN|LOG_PANIC, "Failed to get %s lock for %s: %s", + read_only ? "read" : "write", filename, + errno == ETIMEDOUT ? "timed out" : strerror(errno)); + (void)close(dbblock->lockfd); + errno = 0; /* Indicates locking failure */ + DEBUG(D_hints_lookup) acl_level--; + return NULL; + } + +DEBUG(D_hints_lookup) debug_printf_indent("locked %s\n", filename); + +/* At this point we have an opened and locked separate lock file, that is, +exclusive access to the database, so we can go ahead and open it. If we are +expected to create it, don't do so at first, again so that we can detect +whether we need to change its ownership (see comments about the lock file +above.) There have been regular reports of crashes while opening hints +databases - often this is caused by non-matching db.h and the library. To make +it easy to pin this down, there are now debug statements on either side of the +open call. */ + +snprintf(CS filename, sizeof(filename), "%s/%s", dirname, name); + +priv_drop_temp(exim_uid, exim_gid); +dbblock->dbptr = exim_dbopen(filename, dirname, flags, EXIMDB_MODE); +if (!dbblock->dbptr && errno == ENOENT && flags == O_RDWR) + { + DEBUG(D_hints_lookup) + debug_printf_indent("%s appears not to exist: trying to create\n", filename); + dbblock->dbptr = exim_dbopen(filename, dirname, flags|O_CREAT, EXIMDB_MODE); + } +save_errno = errno; +priv_restore(); + +/* If the open has failed, return NULL, leaving errno set. If lof is TRUE, +log the event - also for debugging - but debug only if the file just doesn't +exist. */ + +if (!dbblock->dbptr) + { + errno = save_errno; + if (lof && save_errno != ENOENT) + log_write(0, LOG_MAIN, "%s", string_open_failed("DB file %s", + filename)); + else + DEBUG(D_hints_lookup) + debug_printf_indent("%s\n", CS string_open_failed("DB file %s", + filename)); + (void)close(dbblock->lockfd); + errno = save_errno; + DEBUG(D_hints_lookup) acl_level--; + return NULL; + } + +DEBUG(D_hints_lookup) + debug_printf_indent("opened hints database %s: flags=%s\n", filename, + flags == O_RDONLY ? "O_RDONLY" + : flags == O_RDWR ? "O_RDWR" + : flags == (O_RDWR|O_CREAT) ? "O_RDWR|O_CREAT" + : "??"); + +/* Pass back the block containing the opened database handle and the open fd +for the lock. */ + +return dbblock; +} + + + + +/************************************************* +* Unlock and close a database file * +*************************************************/ + +/* Closing a file automatically unlocks it, so after closing the database, just +close the lock file. + +Argument: a pointer to an open database block +Returns: nothing +*/ + +void +dbfn_close(open_db *dbblock) +{ +exim_dbclose(dbblock->dbptr); +(void)close(dbblock->lockfd); +DEBUG(D_hints_lookup) + { debug_printf_indent("closed hints database and lockfile\n"); acl_level--; } +} + + + + +/************************************************* +* Read from database file * +*************************************************/ + +/* Passing back the pointer unchanged is useless, because there is +no guarantee of alignment. Since all the records used by Exim need +to be properly aligned to pick out the timestamps, etc., we might as +well do the copying centrally here. + +Most calls don't need the length, so there is a macro called dbfn_read which +has two arguments; it calls this function adding NULL as the third. + +Arguments: + dbblock a pointer to an open database block + key the key of the record to be read + length a pointer to an int into which to return the length, if not NULL + +Returns: a pointer to the retrieved record, or + NULL if the record is not found +*/ + +void * +dbfn_read_with_length(open_db *dbblock, const uschar *key, int *length) +{ +void *yield; +EXIM_DATUM key_datum, result_datum; +int klen = Ustrlen(key) + 1; +uschar * key_copy = store_get(klen, key); + +memcpy(key_copy, key, klen); + +DEBUG(D_hints_lookup) debug_printf_indent("dbfn_read: key=%s\n", key); + +exim_datum_init(&key_datum); /* Some DBM libraries require the datum */ +exim_datum_init(&result_datum); /* to be cleared before use. */ +exim_datum_data_set(&key_datum, key_copy); +exim_datum_size_set(&key_datum, klen); + +if (!exim_dbget(dbblock->dbptr, &key_datum, &result_datum)) return NULL; + +/* Assume the data store could have been tainted. Properly, we should +store the taint status with the data. */ + +yield = store_get(exim_datum_size_get(&result_datum), GET_TAINTED); +memcpy(yield, exim_datum_data_get(&result_datum), exim_datum_size_get(&result_datum)); +if (length) *length = exim_datum_size_get(&result_datum); + +exim_datum_free(&result_datum); /* Some DBM libs require freeing */ +return yield; +} + + +/* Read a record. If the length is not as expected then delete it, write +an error log line, delete the record and return NULL. +Use this for fixed-size records (so not retry or wait records). + +Arguments: + dbblock a pointer to an open database block + key the key of the record to be read + length the expected record length + +Returns: a pointer to the retrieved record, or + NULL if the record is not found/bad +*/ + +void * +dbfn_read_enforce_length(open_db * dbblock, const uschar * key, size_t length) +{ +int rlen; +void * yield = dbfn_read_with_length(dbblock, key, &rlen); + +if (yield) + { + if (rlen == length) return yield; + log_write(0, LOG_MAIN|LOG_PANIC, "Bad db record size for '%s'", key); + dbfn_delete(dbblock, key); + } +return NULL; +} + + +/************************************************* +* Write to database file * +*************************************************/ + +/* +Arguments: + dbblock a pointer to an open database block + key the key of the record to be written + ptr a pointer to the record to be written + length the length of the record to be written + +Returns: the yield of the underlying dbm or db "write" function. If this + is dbm, the value is zero for OK. +*/ + +int +dbfn_write(open_db *dbblock, const uschar *key, void *ptr, int length) +{ +EXIM_DATUM key_datum, value_datum; +dbdata_generic *gptr = (dbdata_generic *)ptr; +int klen = Ustrlen(key) + 1; +uschar * key_copy = store_get(klen, key); + +memcpy(key_copy, key, klen); +gptr->time_stamp = time(NULL); + +DEBUG(D_hints_lookup) debug_printf_indent("dbfn_write: key=%s\n", key); + +exim_datum_init(&key_datum); /* Some DBM libraries require the datum */ +exim_datum_init(&value_datum); /* to be cleared before use. */ +exim_datum_data_set(&key_datum, key_copy); +exim_datum_size_set(&key_datum, klen); +exim_datum_data_set(&value_datum, ptr); +exim_datum_size_set(&value_datum, length); +return exim_dbput(dbblock->dbptr, &key_datum, &value_datum); +} + + + +/************************************************* +* Delete record from database file * +*************************************************/ + +/* +Arguments: + dbblock a pointer to an open database block + key the key of the record to be deleted + +Returns: the yield of the underlying dbm or db "delete" function. +*/ + +int +dbfn_delete(open_db *dbblock, const uschar *key) +{ +int klen = Ustrlen(key) + 1; +uschar * key_copy = store_get(klen, key); +EXIM_DATUM key_datum; + +DEBUG(D_hints_lookup) debug_printf_indent("dbfn_delete: key=%s\n", key); + +memcpy(key_copy, key, klen); +exim_datum_init(&key_datum); /* Some DBM libraries require clearing */ +exim_datum_data_set(&key_datum, key_copy); +exim_datum_size_set(&key_datum, klen); +return exim_dbdel(dbblock->dbptr, &key_datum); +} + + + +/************************************************* +* Scan the keys of a database file * +*************************************************/ + +/* +Arguments: + dbblock a pointer to an open database block + start TRUE if starting a new scan + FALSE if continuing with the current scan + cursor a pointer to a pointer to a cursor anchor, for those dbm libraries + that use the notion of a cursor + +Returns: the next record from the file, or + NULL if there are no more +*/ + +uschar * +dbfn_scan(open_db *dbblock, BOOL start, EXIM_CURSOR **cursor) +{ +EXIM_DATUM key_datum, value_datum; +uschar *yield; + +DEBUG(D_hints_lookup) debug_printf_indent("dbfn_scan\n"); + +/* Some dbm require an initialization */ + +if (start) *cursor = exim_dbcreate_cursor(dbblock->dbptr); + +exim_datum_init(&key_datum); /* Some DBM libraries require the datum */ +exim_datum_init(&value_datum); /* to be cleared before use. */ + +yield = exim_dbscan(dbblock->dbptr, &key_datum, &value_datum, start, *cursor) + ? US exim_datum_data_get(&key_datum) : NULL; + +/* Some dbm require a termination */ + +if (!yield) exim_dbdelete_cursor(*cursor); +return yield; +} + + + +/************************************************* +************************************************** +* Stand-alone test program * +************************************************** +*************************************************/ + +#ifdef STAND_ALONE + +int +main(int argc, char **cargv) +{ +open_db dbblock[8]; +int max_db = sizeof(dbblock)/sizeof(open_db); +int current = -1; +int showtime = 0; +int i; +dbdata_wait *dbwait = NULL; +uschar **argv = USS cargv; +uschar buffer[256]; +uschar structbuffer[1024]; + +if (argc != 2) + { + printf("Usage: test_dbfn directory\n"); + printf("The subdirectory called \"db\" in the given directory is used for\n"); + printf("the files used in this test program.\n"); + return 1; + } + +/* Initialize */ + +spool_directory = argv[1]; +debug_selector = D_all - D_memory; +debug_file = stderr; +big_buffer = malloc(big_buffer_size); + +for (i = 0; i < max_db; i++) dbblock[i].dbptr = NULL; + +printf("\nExim's db functions tester: interface type is %s\n", EXIM_DBTYPE); +printf("DBM library: "); + +#ifdef DB_VERSION_STRING +printf("Berkeley DB: %s\n", DB_VERSION_STRING); +#elif defined(BTREEVERSION) && defined(HASHVERSION) + #ifdef USE_DB + printf("probably Berkeley DB version 1.8x (native mode)\n"); + #else + printf("probably Berkeley DB version 1.8x (compatibility mode)\n"); + #endif +#elif defined(_DBM_RDONLY) || defined(dbm_dirfno) +printf("probably ndbm\n"); +#elif defined(USE_TDB) +printf("using tdb\n"); +#else + #ifdef USE_GDBM + printf("probably GDBM (native mode)\n"); + #else + printf("probably GDBM (compatibility mode)\n"); + #endif +#endif + +/* Test the functions */ + +printf("\nTest the functions\n> "); + +while (Ufgets(buffer, 256, stdin) != NULL) + { + int len = Ustrlen(buffer); + int count = 1; + clock_t start = 1; + clock_t stop = 0; + uschar *cmd = buffer; + while (len > 0 && isspace((uschar)buffer[len-1])) len--; + buffer[len] = 0; + + if (isdigit((uschar)*cmd)) + { + count = Uatoi(cmd); + while (isdigit((uschar)*cmd)) cmd++; + while (isspace((uschar)*cmd)) cmd++; + } + + if (Ustrncmp(cmd, "open", 4) == 0) + { + int i; + open_db *odb; + uschar *s = cmd + 4; + while (isspace((uschar)*s)) s++; + + for (i = 0; i < max_db; i++) + if (dbblock[i].dbptr == NULL) break; + + if (i >= max_db) + { + printf("Too many open databases\n> "); + continue; + } + + start = clock(); + odb = dbfn_open(s, O_RDWR, dbblock + i, TRUE, TRUE); + stop = clock(); + + if (odb) + { + current = i; + printf("opened %d\n", current); + } + /* Other error cases will have written messages */ + else if (errno == ENOENT) + { + printf("open failed: %s%s\n", strerror(errno), + #ifdef USE_DB + " (or other Berkeley DB error)" + #else + "" + #endif + ); + } + } + + else if (Ustrncmp(cmd, "write", 5) == 0) + { + int rc = 0; + uschar *key = cmd + 5; + uschar *data; + + if (current < 0) + { + printf("No current database\n"); + continue; + } + + while (isspace((uschar)*key)) key++; + data = key; + while (*data != 0 && !isspace((uschar)*data)) data++; + *data++ = 0; + while (isspace((uschar)*data)) data++; + + dbwait = (dbdata_wait *)(&structbuffer); + Ustrcpy(dbwait->text, data); + + start = clock(); + while (count-- > 0) + rc = dbfn_write(dbblock + current, key, dbwait, + Ustrlen(data) + sizeof(dbdata_wait)); + stop = clock(); + if (rc != 0) printf("Failed: %s\n", strerror(errno)); + } + + else if (Ustrncmp(cmd, "read", 4) == 0) + { + uschar *key = cmd + 4; + if (current < 0) + { + printf("No current database\n"); + continue; + } + while (isspace((uschar)*key)) key++; + start = clock(); + while (count-- > 0) + dbwait = (dbdata_wait *)dbfn_read_with_length(dbblock+ current, key, NULL); + stop = clock(); + printf("%s\n", (dbwait == NULL)? "" : CS dbwait->text); + } + + else if (Ustrncmp(cmd, "delete", 6) == 0) + { + uschar *key = cmd + 6; + if (current < 0) + { + printf("No current database\n"); + continue; + } + while (isspace((uschar)*key)) key++; + dbfn_delete(dbblock + current, key); + } + + else if (Ustrncmp(cmd, "scan", 4) == 0) + { + EXIM_CURSOR *cursor; + BOOL startflag = TRUE; + uschar *key; + uschar keybuffer[256]; + if (current < 0) + { + printf("No current database\n"); + continue; + } + start = clock(); + while ((key = dbfn_scan(dbblock + current, startflag, &cursor)) != NULL) + { + startflag = FALSE; + Ustrcpy(keybuffer, key); + dbwait = (dbdata_wait *)dbfn_read_with_length(dbblock + current, + keybuffer, NULL); + printf("%s: %s\n", keybuffer, dbwait->text); + } + stop = clock(); + printf("End of scan\n"); + } + + else if (Ustrncmp(cmd, "close", 5) == 0) + { + uschar *s = cmd + 5; + while (isspace((uschar)*s)) s++; + i = Uatoi(s); + if (i >= max_db || dbblock[i].dbptr == NULL) printf("Not open\n"); else + { + start = clock(); + dbfn_close(dbblock + i); + stop = clock(); + dbblock[i].dbptr = NULL; + if (i == current) current = -1; + } + } + + else if (Ustrncmp(cmd, "file", 4) == 0) + { + uschar *s = cmd + 4; + while (isspace((uschar)*s)) s++; + i = Uatoi(s); + if (i >= max_db || dbblock[i].dbptr == NULL) printf("Not open\n"); + else current = i; + } + + else if (Ustrncmp(cmd, "time", 4) == 0) + { + showtime = ~showtime; + printf("Timing %s\n", showtime? "on" : "off"); + } + + else if (Ustrcmp(cmd, "q") == 0 || Ustrncmp(cmd, "quit", 4) == 0) break; + + else if (Ustrncmp(cmd, "help", 4) == 0) + { + printf("close [] close file []\n"); + printf("delete remove record from current file\n"); + printf("file make file current\n"); + printf("open open db file\n"); + printf("q[uit] exit program\n"); + printf("read read record from current file\n"); + printf("scan scan current file\n"); + printf("time time display on/off\n"); + printf("write write record to current file\n"); + } + + else printf("Eh?\n"); + + if (showtime && stop >= start) + printf("start=%d stop=%d difference=%d\n", (int)start, (int)stop, + (int)(stop - start)); + + printf("> "); + } + +for (i = 0; i < max_db; i++) + { + if (dbblock[i].dbptr != NULL) + { + printf("\nClosing %d", i); + dbfn_close(dbblock + i); + } + } + +printf("\n"); +return 0; +} + +#endif + +/* End of dbfn.c */ diff --git a/src/dbfunctions.h b/src/dbfunctions.h new file mode 100644 index 0000000..07d4a62 --- /dev/null +++ b/src/dbfunctions.h @@ -0,0 +1,38 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2022 */ +/* Copyright (c) University of Cambridge 1995 - 2021 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#ifndef DBFUNCTIONS_H +#define DBFUNCTIONS_H + +/* Functions for reading/writing exim database files */ + +void dbfn_close(open_db *); +int dbfn_delete(open_db *, const uschar *); +open_db *dbfn_open(uschar *, int, open_db *, BOOL, BOOL); +void *dbfn_read_with_length(open_db *, const uschar *, int *); +void *dbfn_read_enforce_length(open_db *, const uschar *, size_t); +uschar *dbfn_scan(open_db *, BOOL, EXIM_CURSOR **); +int dbfn_write(open_db *, const uschar *, void *, int); + +/* Macro for the common call to read without wanting to know the length. */ + +#define dbfn_read(a, b) dbfn_read_with_length(a, b, NULL) + +/* Berkeley DB uses a callback function to pass back error details. Its API +changed at release 4.3. */ + +#if defined(USE_DB) && defined(DB_VERSION_STRING) +# if DB_VERSION_MAJOR > 4 || (DB_VERSION_MAJOR == 4 && DB_VERSION_MINOR >= 3) +void dbfn_bdb_error_callback(const DB_ENV *, const char *, const char *); +# else +void dbfn_bdb_error_callback(const char *, char *); +# endif +#endif + +#endif +/* End of dbfunctions.h */ diff --git a/src/dcc.c b/src/dcc.c new file mode 100644 index 0000000..a9124a0 --- /dev/null +++ b/src/dcc.c @@ -0,0 +1,490 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) Wolfgang Breyha 2005 - 2019 + * Vienna University Computer Center + * wbreyha@gmx.net + * See the file NOTICE for conditions of use and distribution. + * + * Copyright (c) The Exim Maintainers 2015 - 2021 + */ + +/* Code for calling dccifd. Called from acl.c. */ + +#include "exim.h" +#ifdef EXPERIMENTAL_DCC +#include "dcc.h" +#include "unistd.h" + +#define DCC_HEADER_LIMIT 120 + +int dcc_ok = 0; +int dcc_rc = 0; + +/* This function takes a file descriptor and a buffer as input and +returns either 0 for success or errno in case of error. */ + +static int flushbuffer +(int socket, gstring *buffer) +{ +int rsp; + +rsp = write(socket, buffer->s, buffer->ptr); +DEBUG(D_acl) + debug_printf("DCC: flushbuffer(): Result of the write() = %d\n", rsp); +if(rsp < 0) + { + DEBUG(D_acl) + debug_printf("DCC: flushbuffer(): Error writing buffer to socket: %s\n", strerror(errno)); + return errno; + } +DEBUG(D_acl) + debug_printf("DCC: flushbuffer(): Wrote buffer to socket:\n%.*s\n", buffer->ptr, buffer->s); +return 0; +} + +int +dcc_process(uschar **listptr) +{ +int sep = 0; +const uschar *list = *listptr; +FILE *data_file; +uschar *dcc_default_ip_option = US"127.0.0.1"; +uschar *dcc_helo_option = US"localhost"; +uschar *xtra_hdrs = NULL; +uschar *override_client_ip = NULL; + +/* from local_scan */ +int dcc_resplen, retval, sockfd, resp; +unsigned int portnr; +struct sockaddr_un serv_addr; +struct sockaddr_in serv_addr_in; +struct hostent *ipaddress; +uschar sockpath[128]; +uschar sockip[40], client_ip[40]; +gstring *dcc_headers; +gstring *sendbuf; +uschar *dcc_return_text = US"''"; +struct header_line *mail_headers; +uschar *dcc_acl_options; +gstring *dcc_xtra_hdrs; +gstring *dcc_header_str; + +/* grep 1st option */ +if ((dcc_acl_options = string_nextinlist(&list, &sep, NULL, 0))) + { + /* parse 1st option */ + if ( strcmpic(dcc_acl_options, US"false") == 0 + || Ustrcmp(dcc_acl_options, "0") == 0) + return FAIL; /* explicitly no matching */ + } +else + return FAIL; /* empty means "don't match anything" */ + +sep = 0; + +/* if we scanned this message last time, just return */ +if (dcc_ok) + return dcc_rc; + +/* open the spooled body */ +for (int i = 0; i < 2; i++) + { + uschar message_subdir[2]; + set_subdir_str(message_subdir, message_id, i); + if ((data_file = Ufopen( + spool_fname(US"input", message_subdir, message_id, US"-D"), "rb"))) + break; + } + +if (!data_file) + { + /* error while spooling */ + log_write(0, LOG_MAIN|LOG_PANIC, + "DCC: error while opening spool file"); + return DEFER; + } + +/* Initialize the variables */ + +bzero(sockip,sizeof(sockip)); +if (dccifd_address) + { + if (dccifd_address[0] == '/') + Ustrncpy(sockpath, dccifd_address, sizeof(sockpath)); + else + if( sscanf(CS dccifd_address, "%s %u", sockip, &portnr) != 2) + { + log_write(0, LOG_MAIN, + "DCC: warning - invalid dccifd address: '%s'", dccifd_address); + (void)fclose(data_file); + return DEFER; + } + } + +/* dcc_headers is what we send as dccifd options - see man dccifd */ +/* We don't support any other option than 'header' so just copy that */ +dcc_headers = string_cat(NULL, dccifd_options); +/* if $acl_m_dcc_override_client_ip is set use it */ +if (((override_client_ip = expand_string(US"$acl_m_dcc_override_client_ip")) != NULL) && + (override_client_ip[0] != '\0')) + { + Ustrncpy(client_ip, override_client_ip, sizeof(client_ip)-1); + DEBUG(D_acl) + debug_printf("DCC: Client IP (overridden): %s\n", client_ip); + } +else if(sender_host_address) + { + /* else if $sender_host_address is available use that? */ + Ustrncpy(client_ip, sender_host_address, sizeof(client_ip)-1); + DEBUG(D_acl) + debug_printf("DCC: Client IP (sender_host_address): %s\n", client_ip); + } +else + { + /* sender_host_address is NULL which means it comes from localhost */ + Ustrncpy(client_ip, dcc_default_ip_option, sizeof(client_ip)-1); + DEBUG(D_acl) + debug_printf("DCC: Client IP (default): %s\n", client_ip); + } +/* build options block */ +dcc_headers = string_append(dcc_headers, 5, US"\n", client_ip, US"\nHELO ", dcc_helo_option, US"\n"); + +/* initialize the other variables */ +mail_headers = header_list; +/* we set the default return value to DEFER */ +retval = DEFER; + +/* send a null return path as "<>". */ +dcc_headers = string_cat (dcc_headers, *sender_address ? sender_address : US"<>"); +dcc_headers = string_catn(dcc_headers, US"\n", 1); + +/************************************** + * Now creating the socket connection * + **************************************/ + +/* If sockip contains an ip, we use a tcp socket, otherwise a UNIX socket */ +if(Ustrcmp(sockip, "")) + { + ipaddress = gethostbyname(CS sockip); + bzero(CS &serv_addr_in, sizeof(serv_addr_in)); + serv_addr_in.sin_family = AF_INET; + bcopy(CS ipaddress->h_addr, CS &serv_addr_in.sin_addr.s_addr, ipaddress->h_length); + serv_addr_in.sin_port = htons(portnr); + if ((sockfd = socket(AF_INET, SOCK_STREAM,0)) < 0) + { + DEBUG(D_acl) + debug_printf("DCC: Creating TCP socket connection failed: %s\n", strerror(errno)); + log_write(0,LOG_PANIC,"DCC: Creating TCP socket connection failed: %s\n", strerror(errno)); + /* if we cannot create the socket, defer the mail */ + (void)fclose(data_file); + return retval; + } + /* Now connecting the socket (INET) */ + if (connect(sockfd, (struct sockaddr *)&serv_addr_in, sizeof(serv_addr_in)) < 0) + { + DEBUG(D_acl) + debug_printf("DCC: Connecting to TCP socket failed: %s\n", strerror(errno)); + log_write(0,LOG_PANIC,"DCC: Connecting to TCP socket failed: %s\n", strerror(errno)); + /* if we cannot contact the socket, defer the mail */ + (void)fclose(data_file); + return retval; + } + } +else + { + /* connecting to the dccifd UNIX socket */ + bzero(&serv_addr, sizeof(serv_addr)); + serv_addr.sun_family = AF_UNIX; + Ustrncpy(US serv_addr.sun_path, sockpath, sizeof(serv_addr.sun_path)); + if ((sockfd = socket(AF_UNIX, SOCK_STREAM,0)) < 0) + { + DEBUG(D_acl) + debug_printf("DCC: Creating UNIX socket connection failed: %s\n", strerror(errno)); + log_write(0,LOG_PANIC,"DCC: Creating UNIX socket connection failed: %s\n", strerror(errno)); + /* if we cannot create the socket, defer the mail */ + (void)fclose(data_file); + return retval; + } + /* Now connecting the socket (UNIX) */ + if (connect(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) + { + DEBUG(D_acl) + debug_printf("DCC: Connecting to UNIX socket failed: %s\n", strerror(errno)); + log_write(0,LOG_PANIC,"DCC: Connecting to UNIX socket failed: %s\n", strerror(errno)); + /* if we cannot contact the socket, defer the mail */ + (void)fclose(data_file); + return retval; + } + } +/* the socket is open, now send the options to dccifd*/ +DEBUG(D_acl) + debug_printf("DCC: -----------------------------------\nDCC: Socket opened; now sending input\n" + "DCC: -----------------------------------\n"); + +/* let's send each of the recipients to dccifd */ +for (int i = 0; i < recipients_count; i++) + { + DEBUG(D_acl) + debug_printf("DCC: recipient = %s\n",recipients_list[i].address); + dcc_headers = string_append(dcc_headers, 2, recipients_list[i].address, "\n"); + } +/* send a blank line between options and message */ +dcc_headers = string_catn(dcc_headers, US"\n", 1); +/* Now we send the input buffer */ +(void) string_from_gstring(dcc_headers); +DEBUG(D_acl) + debug_printf("DCC: ***********************************\nDCC: Sending options:\n%s" + "DCC: ***********************************\n", dcc_headers->s); +if (flushbuffer(sockfd, dcc_headers) != 0) + { + (void)fclose(data_file); + return retval; + } + +/* now send the message */ +/* First send the headers */ +DEBUG(D_acl) + debug_printf("DCC: ***********************************\nDCC: Sending headers:\n"); +sendbuf = string_get(8192); +sendbuf = string_catn(sendbuf, mail_headers->text, mail_headers->slen); +while((mail_headers=mail_headers->next)) + sendbuf = string_catn(sendbuf, mail_headers->text, mail_headers->slen); + +/* a blank line separates header from body */ +sendbuf = string_catn(sendbuf, US"\r\n", 2); +(void) string_from_gstring(sendbuf); +gstring_release_unused(sendbuf); +DEBUG(D_acl) + debug_printf("%sDCC: ***********************************\n", sendbuf->s); +if (flushbuffer(sockfd, sendbuf) != 0) + { + (void)fclose(data_file); + return retval; + } + +/* now send the body */ +DEBUG(D_acl) + debug_printf("DCC: ***********************************\nDCC: Writing body:\n"); +(void)fseek(data_file, SPOOL_DATA_START_OFFSET, SEEK_SET); + +gstring filebuf = { .size = big_buffer_size, .ptr = 0, .s = big_buffer }; + +while((filebuf.ptr = fread(filebuf.s, 1, filebuf.size, data_file)) > 0) + if (flushbuffer(sockfd, &filebuf) != 0) + { + (void)fclose(data_file); + return retval; + } +DEBUG(D_acl) + debug_printf("DCC: ***********************************\n"); + +/* shutdown() the socket */ +if(shutdown(sockfd, SHUT_WR) < 0) + { + DEBUG(D_acl) + debug_printf("DCC: Couldn't shutdown socket: %s\n", strerror(errno)); + log_write(0,LOG_MAIN,"DCC: Couldn't shutdown socket: %s\n", strerror(errno)); + /* If there is a problem with the shutdown() + * defer the mail. */ + (void)fclose(data_file); + return retval; + } +DEBUG(D_acl) + debug_printf("DCC: Input sent.\n" + "DCC: +++++++++++++++++++++++++++++++++++\n" + "DCC: Now receiving output from server\n" + "DCC: -----------------------------------\n"); + +/******************************** + * receiving output from dccifd * + ********************************/ + +/****************************************************************** + * We should get 3 lines: * + * 1/ First line is overall result: either 'A' for Accept, * + * 'R' for Reject, 'S' for accept Some recipients or * + * 'T' for a Temporary error. * + * 2/ Second line contains the list of Accepted/Rejected * + * recipients in the form AARRA (A = accepted, R = rejected). * + * 3/ Third line contains the X-DCC header. * + ******************************************************************/ + +int line = 1; /* we start at the first line of the output */ +int bufoffset; + +dcc_header_str = string_get(DCC_HEADER_LIMIT + 2); +/* Let's read from the socket until there's nothing left to read */ +while((dcc_resplen = read(sockfd, big_buffer, big_buffer_size-1)) > 0) + { + /* fail on read error */ + if(dcc_resplen < 0) + { + DEBUG(D_acl) + debug_printf("DCC: Error reading from socket: %s\n", strerror(errno)); + (void)fclose(data_file); + return retval; + } + /* make the answer 0-terminated. only needed for debug_printf */ + DEBUG(D_acl) + debug_printf("DCC: Length of the output buffer is: %d\nDCC: Output buffer is:\n" + "DCC: -----------------------------------\n%.*s\n" + "DCC: -----------------------------------\n", dcc_resplen, dcc_resplen, big_buffer); + + /* Now let's read each character and see what we've got */ + for(bufoffset = 0; bufoffset < dcc_resplen && line <= 2; bufoffset++) + { + /* First check if we reached the end of the line and + then increment the line counter */ + if(big_buffer[bufoffset] == '\n') + line++; + else + { + /* The first character of the first line is the overall response. If + there's another character on that line it is not correct. */ + if(line == 1) + { + if(bufoffset == 0) + { + /* Now get the value and set the return value accordingly */ + switch (big_buffer[bufoffset]) + { + case 'A': + DEBUG(D_acl) + debug_printf("DCC: Overall result = A\treturning OK\n"); + dcc_return_text = US"Mail accepted by DCC"; + dcc_result = US"A"; + retval = OK; + break; + case 'R': + DEBUG(D_acl) + debug_printf("DCC: Overall result = R\treturning FAIL\n"); + dcc_return_text = US"Rejected by DCC"; + dcc_result = US"R"; + retval = FAIL; + if(sender_host_name) + log_write(0, LOG_MAIN, "H=%s [%s] F=<%s>: rejected by DCC", + sender_host_name, sender_host_address, sender_address); + else + log_write(0, LOG_MAIN, "H=[%s] F=<%s>: rejected by DCC", + sender_host_address, sender_address); + break; + case 'S': + DEBUG(D_acl) + debug_printf("DCC: Overall result = S\treturning OK\n"); + dcc_return_text = US"Not all recipients accepted by DCC"; + /* Since we're in an ACL we want a global result so we accept for all */ + dcc_result = US"A"; + retval = OK; + break; + case 'G': + DEBUG(D_acl) + debug_printf("DCC: Overall result = G\treturning FAIL\n"); + dcc_return_text = US"Greylisted by DCC"; + dcc_result = US"G"; + retval = FAIL; + break; + case 'T': + DEBUG(D_acl) + debug_printf("DCC: Overall result = T\treturning DEFER\n"); + dcc_return_text = US"Temporary error with DCC"; + dcc_result = US"T"; + retval = DEFER; + log_write(0,LOG_MAIN,"Temporary error with DCC: %s\n", big_buffer); + break; + default: + DEBUG(D_acl) + debug_printf("DCC: Overall result = something else\treturning DEFER\n"); + dcc_return_text = US"Unknown DCC response"; + dcc_result = US"T"; + retval = DEFER; + log_write(0,LOG_MAIN,"Unknown DCC response: %s\n", big_buffer); + break; + } + } + else + { + /* We're on the first line but not on the first character, + * there must be something wrong. */ + DEBUG(D_acl) debug_printf("DCC: Line = %d but bufoffset = %d != 0" + " character is %c - This is wrong!\n", line, bufoffset, big_buffer[bufoffset]); + log_write(0,LOG_MAIN,"Wrong header from DCC, output is %s\n", big_buffer); + } + } + else if(line == 2) + { + /* On the second line we get a list of answers for each recipient. We + don't care about it because we're in an acl and take the global result. */ + } + } + } + if(line > 2) + { + /* The third and following lines are the X-DCC header, so we store it in + dcc_header_str up to our limit. */ + /* check if buffer contains the end of the header .."\n\n" and truncate it */ + if ((big_buffer[dcc_resplen-1] == '\n') && + (big_buffer[dcc_resplen-2] == '\n')) + dcc_resplen -= 2; + dcc_resplen -= bufoffset; + if (dcc_header_str->ptr + dcc_resplen > DCC_HEADER_LIMIT) + { + dcc_resplen = DCC_HEADER_LIMIT - dcc_header_str->ptr; + DEBUG(D_acl) debug_printf("DCC: We got more output than we can store" + "in the X-DCC header. Truncating at 120 characters.\n"); + } + dcc_header_str = string_catn(dcc_header_str, &big_buffer[bufoffset], dcc_resplen); + } + } +/* We have read everything from the socket. make sure the header ends with "\n" */ +dcc_header_str = string_catn(dcc_header_str, US"\n", 1); + +(void) string_from_gstring(dcc_header_str); +/* Now let's sum up what we've got. */ +DEBUG(D_acl) + debug_printf("\nDCC: --------------------------\nDCC: Overall result = %d\n" + "DCC: X-DCC header: %sReturn message: %s\nDCC: dcc_result: %s\n", + retval, dcc_header_str->s, dcc_return_text, dcc_result); + +/* We only add the X-DCC header if it starts with X-DCC */ +if(!(Ustrncmp(dcc_header_str->s, "X-DCC", 5))) + { + dcc_header = dcc_header_str->s; + if(dcc_direct_add_header) + { + header_add(' ' , "%s", dcc_header_str->s); +/* since the MIME ACL already writes the .eml file to disk without DCC Header we've to erase it */ + unspool_mbox(); + } + } +else + DEBUG(D_acl) + debug_printf("DCC: Wrong format of the X-DCC header: %.*s\n", dcc_header_str->ptr, dcc_header_str->s); + +/* check if we should add additional headers passed in acl_m_dcc_add_header */ +if (dcc_direct_add_header) + { + if (((xtra_hdrs = expand_string(US"$acl_m_dcc_add_header")) != NULL) && (xtra_hdrs[0] != '\0')) + { + dcc_xtra_hdrs = string_cat(NULL, xtra_hdrs); + if (dcc_xtra_hdrs->s[dcc_xtra_hdrs->ptr - 1] != '\n') + dcc_xtra_hdrs = string_catn(dcc_xtra_hdrs, US"\n", 1); + header_add(' ', "%s", string_from_gstring(dcc_xtra_hdrs)); + DEBUG(D_acl) + debug_printf("DCC: adding additional headers in $acl_m_dcc_add_header: %.*s", dcc_xtra_hdrs->ptr, dcc_xtra_hdrs->s); + } + } + +dcc_ok = 1; +/* Now return to exim main process */ +DEBUG(D_acl) + debug_printf("DCC: Before returning to exim main process:\nDCC: return_text = %s - retval = %d\n" + "DCC: dcc_result = %s\n", dcc_return_text, retval, dcc_result); + +(void)fclose(data_file); +dcc_rc = retval; +return dcc_rc; +} + +#endif diff --git a/src/dcc.h b/src/dcc.h new file mode 100644 index 0000000..9f394f0 --- /dev/null +++ b/src/dcc.h @@ -0,0 +1,16 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* + * Copyright (c) Wolfgang Breyha 2005 + * See the file NOTICE for conditions of use and distribution. + * + * original dccifd_localscan + * Copyright (c) Christopher Bodenstein 2003-2005 + * +*/ + +#ifdef EXPERIMENTAL_DCC +/* currently empty */ +#endif diff --git a/src/debug.c b/src/debug.c new file mode 100644 index 0000000..26d09ea --- /dev/null +++ b/src/debug.c @@ -0,0 +1,498 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2015 - 2022 */ +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +#include "exim.h" + +static uschar debug_buffer[2048]; +static uschar *debug_ptr = debug_buffer; +static int debug_prefix_length = 0; + +static unsigned pretrigger_writeoff; +static unsigned pretrigger_readoff; + + +const uschar * rc_names[] = { /* Mostly for debug output */ + [OK] = US"OK", + [DEFER] = US"DEFER", + [FAIL] = US"FAIL", + [ERROR] = US"ERROR", + [FAIL_FORCED] = US"FAIL_FORCED", + [DECLINE] = US"DECLINE", + [PASS] = US"PASS", + [DISCARD] = US"DISCARD", + [SKIP] = US"SKIP", + [REROUTED] = US"REROUTED", + [PANIC] = US"PANIC", + [BAD64] = US"BAD64", + [UNEXPECTED] = US"UNEXPECTED", + [CANCELLED] = US"CANCELLED", + [FAIL_SEND] = US"FAIL_SEND", + [FAIL_DROP] = US"FAIL_DROP", + [DANE] = US"DANE", +}; + +const uschar * dns_rc_names[] = { + [DNS_SUCCEED] = US"DNS_SUCCEED", + [DNS_NOMATCH] = US"DNS_NOMATCH", + [DNS_NODATA] = US"DNS_NODATA", + [DNS_AGAIN] = US"DNS_AGAIN", + [DNS_FAIL] = US"DNS_FAIL", +}; + + +/************************************************* +* Print tree * +*************************************************/ + +/* Recursive tree-printing subroutine. It uses a static vector of uschar to +hold the line-drawing characters that need to be printed on every line as it +moves down the page. This function is used only in debugging circumstances. The +output is done via debug_printf(). */ + +#define TREE_PRINTLINESIZE 132 /* line size for printing */ +static uschar tree_printline[TREE_PRINTLINESIZE]; + +/* Internal recursive subroutine. + +Arguments: + p tree node + pos amount of indenting & vertical bars to print + barswitch if TRUE print | at the pos value + +Returns: nothing +*/ + +static void +tree_printsub(tree_node * p, int pos, int barswitch) +{ +if (p->right) tree_printsub(p->right, pos+2, 1); +for (int i = 0; i <= pos-1; i++) debug_printf_indent(" %c", tree_printline[i]); +debug_printf_indent(" -->%s [%d]\n", p->name, p->balance); +tree_printline[pos] = barswitch ? '|' : ' '; +if (p->left) + { + tree_printline[pos+2] = '|'; + tree_printsub(p->left, pos+2, 0); + } +} + +/* The external function, with just a tree node argument. */ + +void +debug_print_tree(const char * title, tree_node * p) +{ +debug_printf_indent("%s:\n", title); +for (int i = 0; i < TREE_PRINTLINESIZE; i++) tree_printline[i] = ' '; +if (!p) debug_printf_indent(" Empty Tree\n"); else tree_printsub(p, 0, 0); +debug_printf_indent("---- End of tree ----\n"); +} + + + +/************************************************* +* Print an argv vector * +*************************************************/ + +/* Called when about to obey execv(). + +Argument: the argv vector +Returns: nothing +*/ + +void +debug_print_argv(const uschar ** argv) +{ +debug_printf("exec"); +while (*argv) debug_printf(" %.256s", *argv++); +debug_printf("\n"); +} + + + +/************************************************* +* Expand and print debugging string * +*************************************************/ + +/* The string is expanded and written as debugging output. If +expansion fails, a message is written instead. + +Argument: the string +Returns: nothing +*/ + +void +debug_print_string(uschar *debug_string) +{ +if (!debug_string) return; +HDEBUG(D_any|D_v) + { + uschar *s = expand_string(debug_string); + if (!s) + debug_printf("failed to expand debug_output \"%s\": %s\n", debug_string, + expand_string_message); + else if (s[0] != 0) + debug_printf("%s%s", s, (s[Ustrlen(s)-1] == '\n')? "" : "\n"); + } +} + + + +/************************************************* +* Print current uids and gids * +*************************************************/ + +/* +Argument: an introductory string +Returns: nothing +*/ + +void +debug_print_ids(uschar *s) +{ +debug_printf("%s uid=%ld gid=%ld euid=%ld egid=%ld\n", s, + (long int)getuid(), (long int)getgid(), (long int)geteuid(), + (long int)getegid()); +} + +/************************************************/ + +/* Give a string for a return-code */ + +const uschar * +rc_to_string(int rc) +{ +return rc < 0 || rc >= nelem(rc_names) ? US"?" : rc_names[rc]; +} + + + + + +/************************************************* +* Print debugging message * +*************************************************/ + +/* There are two entries, one for use when being called directly from a +function with a variable argument list, one for prepending an indent. + +If debug_pid is nonzero, print the pid at the start of each line. This is for +tidier output when running parallel remote deliveries with debugging turned on. +Must do the whole thing with a single printf and flush, as otherwise output may +get interleaved. Since some calls to debug_printf() don't end with newline, +we save up the text until we do get the newline. +Take care to not disturb errno. */ + + +/* Debug printf indented by ACL nest depth */ +void +debug_printf_indent(const char * format, ...) +{ +va_list ap; +va_start(ap, format); +debug_vprintf(acl_level + expand_level, format, ap); +va_end(ap); +} + +void +debug_printf(const char *format, ...) +{ +va_list ap; +va_start(ap, format); +debug_vprintf(0, format, ap); +va_end(ap); +} + +void +debug_vprintf(int indent, const char *format, va_list ap) +{ +int save_errno = errno; + +if (!debug_file) return; + +/* Various things can be inserted at the start of a line. Don't use the +tod_stamp() function for the timestamp, because that will overwrite the +timestamp buffer, which may contain something useful. (This was a bug fix: the ++memory debugging with +timestamp did cause a problem.) */ + +if (debug_ptr == debug_buffer) + { + DEBUG(D_timestamp) + { + struct timeval now; + time_t tmp; + struct tm * t; + + gettimeofday(&now, NULL); + tmp = now.tv_sec; + t = f.timestamps_utc ? gmtime(&tmp) : localtime(&tmp); + debug_ptr += sprintf(CS debug_ptr, + LOGGING(millisec) ? "%02d:%02d:%02d.%03d " : "%02d:%02d:%02d ", + t->tm_hour, t->tm_min, t->tm_sec, (int)(now.tv_usec/1000)); + } + + DEBUG(D_pid) + debug_ptr += sprintf(CS debug_ptr, "%5d ", (int)getpid()); + + /* Set up prefix if outputting for host checking and not debugging */ + + if (host_checking && debug_selector == 0) + { + Ustrcpy(debug_ptr, US">>> "); + debug_ptr += 4; + } + + debug_prefix_length = debug_ptr - debug_buffer; + } + +if (indent > 0) + { + for (int i = indent >> 2; i > 0; i--) + DEBUG(D_noutf8) + { + Ustrcpy(debug_ptr, US" !"); + debug_ptr += 4; /* 3 spaces + shriek */ + debug_prefix_length += 4; + } + else + { + Ustrcpy(debug_ptr, US" " UTF8_VERT_2DASH); + debug_ptr += 6; /* 3 spaces + 3 UTF-8 octets */ + debug_prefix_length += 6; + } + + Ustrncpy(debug_ptr, US" ", indent &= 3); + debug_ptr += indent; + debug_prefix_length += indent; + } + +/* Use the lengthchecked formatting routine to ensure that the buffer +does not overflow. Ensure there's space for a newline at the end. +However, use taint-unchecked routines for writing into the buffer +so that we can write tainted info into the static debug_buffer - +we trust that we will never expand the results. */ + + { + gstring gs = { .size = (int)sizeof(debug_buffer) - 1, + .ptr = debug_ptr - debug_buffer, + .s = debug_buffer }; + if (!string_vformat(&gs, SVFMT_TAINT_NOCHK, format, ap)) + { + uschar * s = US"**** debug string too long - truncated ****\n"; + uschar * p = gs.s + gs.ptr; + int maxlen = gs.size - Ustrlen(s) - 2; + if (p > gs.s + maxlen) p = gs.s + maxlen; + if (p > gs.s && p[-1] != '\n') *p++ = '\n'; + Ustrcpy(p, s); + while(*debug_ptr) debug_ptr++; + } + else + { + string_from_gstring(&gs); + debug_ptr = gs.s + gs.ptr; + } + } + +/* Output the line if it is complete. If we added any prefix data and there +are internal newlines, make sure the prefix is on the continuation lines, +as long as there is room in the buffer. We want to do just a single fprintf() +so as to avoid interleaving. */ + +if (debug_ptr[-1] == '\n') + { + if (debug_prefix_length > 0) + { + uschar *p = debug_buffer; + int left = sizeof(debug_buffer) - (debug_ptr - debug_buffer) - 1; + while ((p = Ustrchr(p, '\n') + 1) != debug_ptr && + left >= debug_prefix_length) + { + int len = debug_ptr - p; + memmove(p + debug_prefix_length, p, len + 1); + memmove(p, debug_buffer, debug_prefix_length); + debug_ptr += debug_prefix_length; + left -= debug_prefix_length; + } + } + + if (debug_pretrigger_buf) + { + int needed = Ustrlen(debug_buffer)+1, avail; + char c; + + if (needed > debug_pretrigger_bsize) + needed = debug_pretrigger_bsize; + if ((avail = pretrigger_readoff - pretrigger_writeoff) <= 0) + avail += debug_pretrigger_bsize; + + /* We have a pretrigger set up, trigger not yet hit. Copy the line(s) to the + pretrig buffer, dropping earlier lines if needed but truncating this line if + the pbuf is maxed out. In the PTB the lines are NOT nul-terminated. */ + + while (avail < needed) + do + { + avail++; + c = debug_pretrigger_buf[pretrigger_readoff]; + if (++pretrigger_readoff >= debug_pretrigger_bsize) pretrigger_readoff = 0; + } + while (c && c != '\n' && pretrigger_readoff != pretrigger_writeoff); + + needed--; + for (int i = 0; needed; i++, needed--) + { + debug_pretrigger_buf[pretrigger_writeoff] = debug_buffer[i]; + if (++pretrigger_writeoff >= debug_pretrigger_bsize) pretrigger_writeoff = 0; + } + } + else + { + fprintf(debug_file, "%s", CS debug_buffer); + fflush(debug_file); + } + debug_ptr = debug_buffer; + debug_prefix_length = 0; + } +errno = save_errno; +} + + + +/* Output the details of a socket */ + +void +debug_print_socket(int fd) +{ +struct stat s; +if (fstat(fd, &s) == 0 && (s.st_mode & S_IFMT) == S_IFSOCK) + { + gstring * g = NULL; + int val; + socklen_t vlen = sizeof(val); + struct sockaddr_storage a; + socklen_t alen = sizeof(a); + struct sockaddr_in * sinp = (struct sockaddr_in *)&a; + struct sockaddr_in6 * sin6p = (struct sockaddr_in6 *)&a; + struct sockaddr_un * sunp = (struct sockaddr_un *)&a; + + if (getsockname(fd, (struct sockaddr*)&a, &alen) == 0) + switch (a.ss_family) + { + case AF_INET: + g = string_cat(g, US"domain AF_INET"); + g = string_fmt_append(g, " lcl [%s]:%u", + inet_ntoa(sinp->sin_addr), ntohs(sinp->sin_port)); + alen = sizeof(*sinp); + if (getpeername(fd, (struct sockaddr *)sinp, &alen) == 0) + g = string_fmt_append(g, " rmt [%s]:%u", + inet_ntoa(sinp->sin_addr), ntohs(sinp->sin_port)); + break; + case AF_INET6: + { + uschar buf[46]; + g = string_cat(g, US"domain AF_INET6"); + g = string_fmt_append(g, " lcl [%s]:%u", + inet_ntop(AF_INET6, &sin6p->sin6_addr, CS buf, sizeof(buf)), + ntohs(sin6p->sin6_port)); + alen = sizeof(*sin6p); + if (getpeername(fd, (struct sockaddr *)sin6p, &alen) == 0) + g = string_fmt_append(g, " rmt [%s]:%u", + inet_ntop(AF_INET6, &sin6p->sin6_addr, CS buf, sizeof(buf)), + ntohs(sin6p->sin6_port)); + break; + } + case AF_UNIX: + g = string_cat(g, US"domain AF_UNIX"); + if (alen > sizeof(sa_family_t)) /* not unix(7) "unnamed socket" */ + g = string_fmt_append(g, " lcl %s%s", + sunp->sun_path[0] ? US"" : US"@", + sunp->sun_path[0] ? sunp->sun_path : sunp->sun_path+1); + alen = sizeof(*sunp); + if (getpeername(fd, (struct sockaddr *)sunp, &alen) == 0) + g = string_fmt_append(g, " rmt %s%s", + sunp->sun_path[0] ? US"" : US"@", + sunp->sun_path[0] ? sunp->sun_path : sunp->sun_path+1); + break; + default: + g = string_fmt_append(g, "domain %u", sinp->sin_family); + break; + } + if (getsockopt(fd, SOL_SOCKET, SO_TYPE, &val, &vlen) == 0) + switch (val) + { + case SOCK_STREAM: g = string_cat(g, US" type SOCK_STREAM"); break; + case SOCK_DGRAM: g = string_cat(g, US" type SOCK_DGRAM"); break; + default: g = string_fmt_append(g, " type %d", val); break; + } +#ifdef SO_PROTOCOL + if (getsockopt(fd, SOL_SOCKET, SO_PROTOCOL, &val, &vlen) == 0) + { + struct protoent * p = getprotobynumber(val); + g = p + ? string_fmt_append(g, " proto %s", p->p_name) + : string_fmt_append(g, " proto %d", val); + } +#endif + debug_printf_indent(" socket: %s\n", string_from_gstring(g)); + } +else + debug_printf_indent(" fd st_mode 0%o\n", s.st_mode); +} + + +/**************************************************************/ +/* Pretrigger handling for debug. The debug_printf implementation +diverts output to a circular buffer if the buffer is set up. +The routines here set up the buffer, and unload it to file (and release it). +What ends up in the buffer is subject to the usual debug_selector. */ + +void +debug_pretrigger_setup(const uschar * size_string) +{ +long size = Ustrtol(size_string, NULL, 0); +if (size > 0) + { + unsigned bufsize = MIN(size, 16384); + + dtrigger_selector |= BIT(DTi_pretrigger); + if (debug_pretrigger_buf) store_free(debug_pretrigger_buf); + debug_pretrigger_buf = store_malloc((size_t)(debug_pretrigger_bsize = bufsize)); + pretrigger_readoff = pretrigger_writeoff = 0; + } +} + +void +debug_trigger_fire(void) +{ +int nbytes; + +if (!debug_pretrigger_buf) return; + +if (debug_file && (nbytes = pretrigger_writeoff - pretrigger_readoff) != 0) + if (nbytes > 0) + fwrite(debug_pretrigger_buf + pretrigger_readoff, 1, nbytes, debug_file); + else + { + fwrite(debug_pretrigger_buf + pretrigger_readoff, 1, + debug_pretrigger_bsize - pretrigger_readoff, debug_file); + fwrite(debug_pretrigger_buf, 1, pretrigger_writeoff, debug_file); + } + +debug_pretrigger_discard(); +} + +void +debug_pretrigger_discard(void) +{ +if (debug_pretrigger_buf) store_free(debug_pretrigger_buf); +debug_pretrigger_buf = NULL; +dtrigger_selector = 0; +} + + +/* End of debug.c */ diff --git a/src/deliver.c b/src/deliver.c new file mode 100644 index 0000000..8a9a174 --- /dev/null +++ b/src/deliver.c @@ -0,0 +1,8642 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* The main code for delivering a message. */ + + +#include "exim.h" +#include "transports/smtp.h" +#include +#include + + +/* Data block for keeping track of subprocesses for parallel remote +delivery. */ + +typedef struct pardata { + address_item *addrlist; /* chain of addresses */ + address_item *addr; /* next address data expected for */ + pid_t pid; /* subprocess pid */ + int fd; /* pipe fd for getting result from subprocess */ + int transport_count; /* returned transport count value */ + BOOL done; /* no more data needed */ + uschar *msg; /* error message */ + uschar *return_path; /* return_path for these addresses */ +} pardata; + +/* Values for the process_recipients variable */ + +enum { RECIP_ACCEPT, RECIP_IGNORE, RECIP_DEFER, + RECIP_FAIL, RECIP_FAIL_FILTER, RECIP_FAIL_TIMEOUT, + RECIP_FAIL_LOOP}; + +/* Mutually recursive functions for marking addresses done. */ + +static void child_done(address_item *, uschar *); +static void address_done(address_item *, uschar *); + +/* Table for turning base-62 numbers into binary */ + +static uschar tab62[] = + {0,1,2,3,4,5,6,7,8,9,0,0,0,0,0,0, /* 0-9 */ + 0,10,11,12,13,14,15,16,17,18,19,20, /* A-K */ + 21,22,23,24,25,26,27,28,29,30,31,32, /* L-W */ + 33,34,35, 0, 0, 0, 0, 0, /* X-Z */ + 0,36,37,38,39,40,41,42,43,44,45,46, /* a-k */ + 47,48,49,50,51,52,53,54,55,56,57,58, /* l-w */ + 59,60,61}; /* x-z */ + + +/************************************************* +* Local static variables * +*************************************************/ + +/* addr_duplicate is global because it needs to be seen from the Envelope-To +writing code. */ + +static address_item *addr_defer = NULL; +static address_item *addr_failed = NULL; +static address_item *addr_fallback = NULL; +static address_item *addr_local = NULL; +static address_item *addr_new = NULL; +static address_item *addr_remote = NULL; +static address_item *addr_route = NULL; +static address_item *addr_succeed = NULL; +static address_item *addr_senddsn = NULL; + +static FILE *message_log = NULL; +static BOOL update_spool; +static BOOL remove_journal; +static int parcount = 0; +static pardata *parlist = NULL; +static struct pollfd *parpoll; +static int return_count; +static uschar *frozen_info = US""; +static uschar *used_return_path = NULL; + + + +/************************************************* +* read as much as requested * +*************************************************/ + +/* The syscall read(2) doesn't always returns as much as we want. For +several reasons it might get less. (Not talking about signals, as syscalls +are restartable). When reading from a network or pipe connection the sender +might send in smaller chunks, with delays between these chunks. The read(2) +may return such a chunk. + +The more the writer writes and the smaller the pipe between write and read is, +the more we get the chance of reading leass than requested. (See bug 2130) + +This function read(2)s until we got all the data we *requested*. + +Note: This function may block. Use it only if you're sure about the +amount of data you will get. + +Argument: + fd the file descriptor to read from + buffer pointer to a buffer of size len + len the requested(!) amount of bytes + +Returns: the amount of bytes read +*/ +static ssize_t +readn(int fd, void * buffer, size_t len) +{ +uschar * next = buffer; +uschar * end = next + len; + +while (next < end) + { + ssize_t got = read(fd, next, end - next); + + /* I'm not sure if there are signals that can interrupt us, + for now I assume the worst */ + if (got == -1 && errno == EINTR) continue; + if (got <= 0) return next - US buffer; + next += got; + } + +return len; +} + + +/************************************************* +* Make a new address item * +*************************************************/ + +/* This function gets the store and initializes with default values. The +transport_return value defaults to DEFER, so that any unexpected failure to +deliver does not wipe out the message. The default unique string is set to a +copy of the address, so that its domain can be lowercased. + +Argument: + address the RFC822 address string + copy force a copy of the address + +Returns: a pointer to an initialized address_item +*/ + +address_item * +deliver_make_addr(uschar *address, BOOL copy) +{ +address_item * addr = store_get(sizeof(address_item), GET_UNTAINTED); +*addr = address_defaults; +if (copy) address = string_copy(address); +addr->address = address; +addr->unique = string_copy(address); +return addr; +} + + + + +/************************************************* +* Set expansion values for an address * +*************************************************/ + +/* Certain expansion variables are valid only when handling an address or +address list. This function sets them up or clears the values, according to its +argument. + +Arguments: + addr the address in question, or NULL to clear values +Returns: nothing +*/ + +void +deliver_set_expansions(address_item *addr) +{ +if (!addr) + { + const uschar ***p = address_expansions; + while (*p) **p++ = NULL; + return; + } + +/* Exactly what gets set depends on whether there is one or more addresses, and +what they contain. These first ones are always set, taking their values from +the first address. */ + +if (!addr->host_list) + { + deliver_host = deliver_host_address = US""; + deliver_host_port = 0; + } +else + { + deliver_host = addr->host_list->name; + deliver_host_address = addr->host_list->address; + deliver_host_port = addr->host_list->port; + } + +deliver_recipients = addr; +deliver_address_data = addr->prop.address_data; +deliver_domain_data = addr->prop.domain_data; +deliver_localpart_data = addr->prop.localpart_data; +router_var = addr->prop.variables; + +/* These may be unset for multiple addresses */ + +deliver_domain = addr->domain; +self_hostname = addr->self_hostname; + +#ifdef EXPERIMENTAL_BRIGHTMAIL +bmi_deliver = 1; /* deliver by default */ +bmi_alt_location = NULL; +bmi_base64_verdict = NULL; +bmi_base64_tracker_verdict = NULL; +#endif + +/* If there's only one address we can set everything. */ + +if (!addr->next) + { + address_item *addr_orig; + + deliver_localpart = addr->local_part; + deliver_localpart_prefix = addr->prefix; + deliver_localpart_prefix_v = addr->prefix_v; + deliver_localpart_suffix = addr->suffix; + deliver_localpart_suffix_v = addr->suffix_v; + + for (addr_orig = addr; addr_orig->parent; addr_orig = addr_orig->parent) ; + deliver_domain_orig = addr_orig->domain; + + /* Re-instate any prefix and suffix in the original local part. In all + normal cases, the address will have a router associated with it, and we can + choose the caseful or caseless version accordingly. However, when a system + filter sets up a pipe, file, or autoreply delivery, no router is involved. + In this case, though, there won't be any prefix or suffix to worry about. */ + + deliver_localpart_orig = !addr_orig->router + ? addr_orig->local_part + : addr_orig->router->caseful_local_part + ? addr_orig->cc_local_part + : addr_orig->lc_local_part; + + /* If there's a parent, make its domain and local part available, and if + delivering to a pipe or file, or sending an autoreply, get the local + part from the parent. For pipes and files, put the pipe or file string + into address_pipe and address_file. */ + + if (addr->parent) + { + deliver_domain_parent = addr->parent->domain; + deliver_localpart_parent = !addr->parent->router + ? addr->parent->local_part + : addr->parent->router->caseful_local_part + ? addr->parent->cc_local_part + : addr->parent->lc_local_part; + + /* File deliveries have their own flag because they need to be picked out + as special more often. */ + + if (testflag(addr, af_pfr)) + { + if (testflag(addr, af_file)) address_file = addr->local_part; + else if (deliver_localpart[0] == '|') address_pipe = addr->local_part; + deliver_localpart = addr->parent->local_part; + deliver_localpart_prefix = addr->parent->prefix; + deliver_localpart_prefix_v = addr->parent->prefix_v; + deliver_localpart_suffix = addr->parent->suffix; + deliver_localpart_suffix_v = addr->parent->suffix_v; + } + } + +#ifdef EXPERIMENTAL_BRIGHTMAIL + /* Set expansion variables related to Brightmail AntiSpam */ + bmi_base64_verdict = bmi_get_base64_verdict(deliver_localpart_orig, deliver_domain_orig); + bmi_base64_tracker_verdict = bmi_get_base64_tracker_verdict(bmi_base64_verdict); + /* get message delivery status (0 - don't deliver | 1 - deliver) */ + bmi_deliver = bmi_get_delivery_status(bmi_base64_verdict); + /* if message is to be delivered, get eventual alternate location */ + if (bmi_deliver == 1) + bmi_alt_location = bmi_get_alt_location(bmi_base64_verdict); +#endif + + } + +/* For multiple addresses, don't set local part, and leave the domain and +self_hostname set only if it is the same for all of them. It is possible to +have multiple pipe and file addresses, but only when all addresses have routed +to the same pipe or file. */ + +else + { + if (testflag(addr, af_pfr)) + { + if (testflag(addr, af_file)) address_file = addr->local_part; + else if (addr->local_part[0] == '|') address_pipe = addr->local_part; + } + for (address_item * addr2 = addr->next; addr2; addr2 = addr2->next) + { + if (deliver_domain && Ustrcmp(deliver_domain, addr2->domain) != 0) + deliver_domain = NULL; + if ( self_hostname + && ( !addr2->self_hostname + || Ustrcmp(self_hostname, addr2->self_hostname) != 0 + ) ) + self_hostname = NULL; + if (!deliver_domain && !self_hostname) break; + } + } +} + + + + +/************************************************* +* Open a msglog file * +*************************************************/ + +/* This function is used both for normal message logs, and for files in the +msglog directory that are used to catch output from pipes. Try to create the +directory if it does not exist. From release 4.21, normal message logs should +be created when the message is received. + +Called from deliver_message(), can be operating as root. + +Argument: + filename the file name + mode the mode required + error used for saying what failed + +Returns: a file descriptor, or -1 (with errno set) +*/ + +static int +open_msglog_file(uschar *filename, int mode, uschar **error) +{ +if (Ustrstr(filename, US"/../")) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, + "Attempt to open msglog file path with upward-traversal: '%s'\n", filename); + +for (int i = 2; i > 0; i--) + { + int fd = Uopen(filename, +#ifdef O_CLOEXEC + O_CLOEXEC | +#endif +#ifdef O_NOFOLLOW + O_NOFOLLOW | +#endif + O_WRONLY|O_APPEND|O_CREAT, mode); + if (fd >= 0) + { + /* Set the close-on-exec flag and change the owner to the exim uid/gid (this + function is called as root). Double check the mode, because the group setting + doesn't always get set automatically. */ + +#ifndef O_CLOEXEC + (void)fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) | FD_CLOEXEC); +#endif + if (exim_fchown(fd, exim_uid, exim_gid, filename) < 0) + { + *error = US"chown"; + return -1; + } + if (fchmod(fd, mode) < 0) + { + *error = US"chmod"; + return -1; + } + return fd; + } + if (errno != ENOENT) + break; + + (void)directory_make(spool_directory, + spool_sname(US"msglog", message_subdir), + MSGLOG_DIRECTORY_MODE, TRUE); + } + +*error = US"create or open"; +return -1; +} + + + + +/************************************************* +* Write to msglog if required * +*************************************************/ + +/* Write to the message log, if configured. This function may also be called +from transports. + +Arguments: + format a string format + +Returns: nothing +*/ + +void +deliver_msglog(const char *format, ...) +{ +va_list ap; +if (!message_logs) return; +va_start(ap, format); +vfprintf(message_log, format, ap); +fflush(message_log); +va_end(ap); +} + + + + +/************************************************* +* Replicate status for batch * +*************************************************/ + +/* When a transport handles a batch of addresses, it may treat them +individually, or it may just put the status in the first one, and return FALSE, +requesting that the status be copied to all the others externally. This is the +replication function. As well as the status, it copies the transport pointer, +which may have changed if appendfile passed the addresses on to a different +transport. + +Argument: pointer to the first address in a chain +Returns: nothing +*/ + +static void +replicate_status(address_item *addr) +{ +for (address_item * addr2 = addr->next; addr2; addr2 = addr2->next) + { + addr2->transport = addr->transport; + addr2->transport_return = addr->transport_return; + addr2->basic_errno = addr->basic_errno; + addr2->more_errno = addr->more_errno; + addr2->delivery_time = addr->delivery_time; + addr2->special_action = addr->special_action; + addr2->message = addr->message; + addr2->user_message = addr->user_message; + } +} + + + +/************************************************* +* Compare lists of hosts * +*************************************************/ + +/* This function is given two pointers to chains of host items, and it yields +TRUE if the lists refer to the same hosts in the same order, except that + +(1) Multiple hosts with the same non-negative MX values are permitted to appear + in different orders. Round-robinning nameservers can cause this to happen. + +(2) Multiple hosts with the same negative MX values less than MX_NONE are also + permitted to appear in different orders. This is caused by randomizing + hosts lists. + +This enables Exim to use a single SMTP transaction for sending to two entirely +different domains that happen to end up pointing at the same hosts. + +We do not try to batch up different A-record host names that refer to the +same IP. + +Arguments: + one points to the first host list + two points to the second host list + +Returns: TRUE if the lists refer to the same host set +*/ + +static BOOL +same_hosts(host_item *one, host_item *two) +{ +while (one && two) + { + if (Ustrcmp(one->name, two->name) != 0) + { + int mx = one->mx; + host_item *end_one = one; + host_item *end_two = two; + + /* Batch up only if there was no MX and the list was not randomized */ + + if (mx == MX_NONE) return FALSE; + + /* Find the ends of the shortest sequence of identical MX values */ + + while ( end_one->next && end_one->next->mx == mx + && end_two->next && end_two->next->mx == mx) + { + end_one = end_one->next; + end_two = end_two->next; + } + + /* If there aren't any duplicates, there's no match. */ + + if (end_one == one) return FALSE; + + /* For each host in the 'one' sequence, check that it appears in the 'two' + sequence, returning FALSE if not. */ + + for (;;) + { + host_item *hi; + for (hi = two; hi != end_two->next; hi = hi->next) + if (Ustrcmp(one->name, hi->name) == 0) break; + if (hi == end_two->next) return FALSE; + if (one == end_one) break; + one = one->next; + } + + /* All the hosts in the 'one' sequence were found in the 'two' sequence. + Ensure both are pointing at the last host, and carry on as for equality. */ + + two = end_two; + } + + /* if the names matched but ports do not, mismatch */ + else if (one->port != two->port) + return FALSE; + +#ifdef SUPPORT_DANE + /* DNSSEC equality */ + if (one->dnssec != two->dnssec) return FALSE; +#endif + + /* Hosts matched */ + one = one->next; + two = two->next; + } + +/* True if both are NULL */ + +return (one == two); +} + + + +/************************************************* +* Compare header lines * +*************************************************/ + +/* This function is given two pointers to chains of header items, and it yields +TRUE if they are the same header texts in the same order. + +Arguments: + one points to the first header list + two points to the second header list + +Returns: TRUE if the lists refer to the same header set +*/ + +static BOOL +same_headers(header_line *one, header_line *two) +{ +for (;; one = one->next, two = two->next) + { + if (one == two) return TRUE; /* Includes the case where both NULL */ + if (!one || !two) return FALSE; + if (Ustrcmp(one->text, two->text) != 0) return FALSE; + } +} + + + +/************************************************* +* Compare string settings * +*************************************************/ + +/* This function is given two pointers to strings, and it returns +TRUE if they are the same pointer, or if the two strings are the same. + +Arguments: + one points to the first string + two points to the second string + +Returns: TRUE or FALSE +*/ + +static BOOL +same_strings(uschar *one, uschar *two) +{ +if (one == two) return TRUE; /* Includes the case where both NULL */ +if (!one || !two) return FALSE; +return (Ustrcmp(one, two) == 0); +} + + + +/************************************************* +* Compare uid/gid for addresses * +*************************************************/ + +/* This function is given a transport and two addresses. It yields TRUE if the +uid/gid/initgroups settings for the two addresses are going to be the same when +they are delivered. + +Arguments: + tp the transort + addr1 the first address + addr2 the second address + +Returns: TRUE or FALSE +*/ + +static BOOL +same_ugid(transport_instance *tp, address_item *addr1, address_item *addr2) +{ +if ( !tp->uid_set && !tp->expand_uid + && !tp->deliver_as_creator + && ( testflag(addr1, af_uid_set) != testflag(addr2, af_gid_set) + || ( testflag(addr1, af_uid_set) + && ( addr1->uid != addr2->uid + || testflag(addr1, af_initgroups) != testflag(addr2, af_initgroups) + ) ) ) ) + return FALSE; + +if ( !tp->gid_set && !tp->expand_gid + && ( testflag(addr1, af_gid_set) != testflag(addr2, af_gid_set) + || ( testflag(addr1, af_gid_set) + && addr1->gid != addr2->gid + ) ) ) + return FALSE; + +return TRUE; +} + + + + +/************************************************* +* Record that an address is complete * +*************************************************/ + +/* This function records that an address is complete. This is straightforward +for most addresses, where the unique address is just the full address with the +domain lower cased. For homonyms (addresses that are the same as one of their +ancestors) their are complications. Their unique addresses have \x\ prepended +(where x = 0, 1, 2...), so that de-duplication works correctly for siblings and +cousins. + +Exim used to record the unique addresses of homonyms as "complete". This, +however, fails when the pattern of redirection varies over time (e.g. if taking +unseen copies at only some times of day) because the prepended numbers may vary +from one delivery run to the next. This problem is solved by never recording +prepended unique addresses as complete. Instead, when a homonymic address has +actually been delivered via a transport, we record its basic unique address +followed by the name of the transport. This is checked in subsequent delivery +runs whenever an address is routed to a transport. + +If the completed address is a top-level one (has no parent, which means it +cannot be homonymic) we also add the original address to the non-recipients +tree, so that it gets recorded in the spool file and therefore appears as +"done" in any spool listings. The original address may differ from the unique +address in the case of the domain. + +Finally, this function scans the list of duplicates, marks as done any that +match this address, and calls child_done() for their ancestors. + +Arguments: + addr address item that has been completed + now current time as a string + +Returns: nothing +*/ + +static void +address_done(address_item *addr, uschar *now) +{ +update_spool = TRUE; /* Ensure spool gets updated */ + +/* Top-level address */ + +if (!addr->parent) + { + tree_add_nonrecipient(addr->unique); + tree_add_nonrecipient(addr->address); + } + +/* Homonymous child address */ + +else if (testflag(addr, af_homonym)) + { + if (addr->transport) + tree_add_nonrecipient( + string_sprintf("%s/%s", addr->unique + 3, addr->transport->name)); + } + +/* Non-homonymous child address */ + +else tree_add_nonrecipient(addr->unique); + +/* Check the list of duplicate addresses and ensure they are now marked +done as well. */ + +for (address_item * dup = addr_duplicate; dup; dup = dup->next) + if (Ustrcmp(addr->unique, dup->unique) == 0) + { + tree_add_nonrecipient(dup->unique); + child_done(dup, now); + } +} + + + + +/************************************************* +* Decrease counts in parents and mark done * +*************************************************/ + +/* This function is called when an address is complete. If there is a parent +address, its count of children is decremented. If there are still other +children outstanding, the function exits. Otherwise, if the count has become +zero, address_done() is called to mark the parent and its duplicates complete. +Then loop for any earlier ancestors. + +Arguments: + addr points to the completed address item + now the current time as a string, for writing to the message log + +Returns: nothing +*/ + +static void +child_done(address_item *addr, uschar *now) +{ +while (addr->parent) + { + address_item *aa; + + addr = addr->parent; + if (--addr->child_count > 0) return; /* Incomplete parent */ + address_done(addr, now); + + /* Log the completion of all descendents only when there is no ancestor with + the same original address. */ + + for (aa = addr->parent; aa; aa = aa->parent) + if (Ustrcmp(aa->address, addr->address) == 0) break; + if (aa) continue; + + deliver_msglog("%s %s: children all complete\n", now, addr->address); + DEBUG(D_deliver) debug_printf("%s: children all complete\n", addr->address); + } +} + + + +/************************************************* +* Delivery logging support functions * +*************************************************/ + +/* The LOGGING() checks in d_log_interface() are complicated for backwards +compatibility. When outgoing interface logging was originally added, it was +conditional on just incoming_interface (which is off by default). The +outgoing_interface option is on by default to preserve this behaviour, but +you can enable incoming_interface and disable outgoing_interface to get I= +fields on incoming lines only. + +Arguments: + g The log line + addr The address to be logged + +Returns: New value for s +*/ + +static gstring * +d_log_interface(gstring * g) +{ +if (LOGGING(incoming_interface) && LOGGING(outgoing_interface) + && sending_ip_address) + { + g = string_fmt_append(g, " I=[%s]", sending_ip_address); + if (LOGGING(outgoing_port)) + g = string_fmt_append(g, ":%d", sending_port); + } +return g; +} + + + +static gstring * +d_hostlog(gstring * g, address_item * addr) +{ +host_item * h = addr->host_used; + +g = string_append(g, 2, US" H=", h->name); + +if (LOGGING(dnssec) && h->dnssec == DS_YES) + g = string_catn(g, US" DS", 3); + +g = string_append(g, 3, US" [", h->address, US"]"); + +if (LOGGING(outgoing_port)) + g = string_fmt_append(g, ":%d", h->port); + +if (continue_sequence > 1) /*XXX this is wrong for a dropped proxyconn. Would have to pass back from transport */ + g = string_catn(g, US"*", 1); + +#ifdef SUPPORT_SOCKS +if (LOGGING(proxy) && proxy_local_address) + { + g = string_append(g, 3, US" PRX=[", proxy_local_address, US"]"); + if (LOGGING(outgoing_port)) + g = string_fmt_append(g, ":%d", proxy_local_port); + } +#endif + +g = d_log_interface(g); + +if (testflag(addr, af_tcp_fastopen)) + g = string_catn(g, US" TFO*", testflag(addr, af_tcp_fastopen_data) ? 5 : 4); + +return g; +} + + + + + +#ifndef DISABLE_TLS +static gstring * +d_tlslog(gstring * g, address_item * addr) +{ +if (LOGGING(tls_cipher) && addr->cipher) + { + g = string_append(g, 2, US" X=", addr->cipher); +#ifndef DISABLE_TLS_RESUME + if (LOGGING(tls_resumption) && testflag(addr, af_tls_resume)) + g = string_catn(g, US"*", 1); +#endif + } +if (LOGGING(tls_certificate_verified) && addr->cipher) + g = string_append(g, 2, US" CV=", + testflag(addr, af_cert_verified) + ? +#ifdef SUPPORT_DANE + testflag(addr, af_dane_verified) + ? "dane" + : +#endif + "yes" + : "no"); +if (LOGGING(tls_peerdn) && addr->peerdn) + g = string_append(g, 3, US" DN=\"", string_printing(addr->peerdn), US"\""); +return g; +} +#endif + + + + +#ifndef DISABLE_EVENT +/* Distribute a named event to any listeners. + +Args: action config option specifying listener + event name of the event + ev_data associated data for the event + errnop pointer to errno for modification, or null + +Return: string expansion from listener, or NULL +*/ + +uschar * +event_raise(uschar * action, const uschar * event, uschar * ev_data, int * errnop) +{ +uschar * s; +if (action) + { + DEBUG(D_deliver) + debug_printf("Event(%s): event_action=|%s| delivery_IP=%s\n", + event, + action, deliver_host_address); + + event_name = event; + event_data = ev_data; + + if (!(s = expand_string(action)) && *expand_string_message) + log_write(0, LOG_MAIN|LOG_PANIC, + "failed to expand event_action %s in %s: %s\n", + event, transport_name ? transport_name : US"main", expand_string_message); + + event_name = event_data = NULL; + + /* If the expansion returns anything but an empty string, flag for + the caller to modify his normal processing + */ + if (s && *s) + { + DEBUG(D_deliver) + debug_printf("Event(%s): event_action returned \"%s\"\n", event, s); + if (errnop) + *errnop = ERRNO_EVENT; + return s; + } + } +return NULL; +} + +void +msg_event_raise(const uschar * event, const address_item * addr) +{ +const uschar * save_domain = deliver_domain; +uschar * save_local = deliver_localpart; +const uschar * save_host = deliver_host; +const uschar * save_address = deliver_host_address; +const int save_port = deliver_host_port; + +router_name = addr->router ? addr->router->name : NULL; +deliver_domain = addr->domain; +deliver_localpart = addr->local_part; +deliver_host = addr->host_used ? addr->host_used->name : NULL; + +if (!addr->transport) + { + if (Ustrcmp(event, "msg:fail:delivery") == 0) + { + /* An address failed with no transport involved. This happens when + a filter was used which triggered a fail command (in such a case + a transport isn't needed). Convert it to an internal fail event. */ + + (void) event_raise(event_action, US"msg:fail:internal", addr->message, NULL); + } + } +else + { + transport_name = addr->transport->name; + + (void) event_raise(addr->transport->event_action, event, + addr->host_used + || Ustrcmp(addr->transport->driver_name, "smtp") == 0 + || Ustrcmp(addr->transport->driver_name, "lmtp") == 0 + || Ustrcmp(addr->transport->driver_name, "autoreply") == 0 + ? addr->message : NULL, + NULL); + } + +deliver_host_port = save_port; +deliver_host_address = save_address; +deliver_host = save_host; +deliver_localpart = save_local; +deliver_domain = save_domain; +router_name = transport_name = NULL; +} +#endif /*DISABLE_EVENT*/ + + + +/******************************************************************************/ + + +/************************************************* +* Generate local part for logging * +*************************************************/ + +static uschar * +string_get_lpart_sub(const address_item * addr, uschar * s) +{ +#ifdef SUPPORT_I18N +if (testflag(addr, af_utf8_downcvt)) + { + uschar * t = string_localpart_utf8_to_alabel(s, NULL); + return t ? t : s; /* t is NULL on a failed conversion */ + } +#endif +return s; +} + +/* This function is a subroutine for use in string_log_address() below. + +Arguments: + addr the address being logged + yield the current dynamic buffer pointer + +Returns: the new value of the buffer pointer +*/ + +static gstring * +string_get_localpart(address_item * addr, gstring * yield) +{ +uschar * s; + +if (testflag(addr, af_include_affixes) && (s = addr->prefix)) + yield = string_cat(yield, string_get_lpart_sub(addr, s)); + +yield = string_cat(yield, string_get_lpart_sub(addr, addr->local_part)); + +if (testflag(addr, af_include_affixes) && (s = addr->suffix)) + yield = string_cat(yield, string_get_lpart_sub(addr, s)); + +return yield; +} + + +/************************************************* +* Generate log address list * +*************************************************/ + +/* This function generates a list consisting of an address and its parents, for +use in logging lines. For saved onetime aliased addresses, the onetime parent +field is used. If the address was delivered by a transport with rcpt_include_ +affixes set, the af_include_affixes bit will be set in the address. In that +case, we include the affixes here too. + +Arguments: + g points to growing-string struct + addr bottom (ultimate) address + all_parents if TRUE, include all parents + success TRUE for successful delivery + +Returns: a growable string in dynamic store +*/ + +static gstring * +string_log_address(gstring * g, + address_item *addr, BOOL all_parents, BOOL success) +{ +BOOL add_topaddr = TRUE; +address_item *topaddr; + +/* Find the ultimate parent */ + +for (topaddr = addr; topaddr->parent; topaddr = topaddr->parent) ; + +/* We start with just the local part for pipe, file, and reply deliveries, and +for successful local deliveries from routers that have the log_as_local flag +set. File deliveries from filters can be specified as non-absolute paths in +cases where the transport is going to complete the path. If there is an error +before this happens (expansion failure) the local part will not be updated, and +so won't necessarily look like a path. Add extra text for this case. */ + +if ( testflag(addr, af_pfr) + || ( success + && addr->router && addr->router->log_as_local + && addr->transport && addr->transport->info->local + ) ) + { + if (testflag(addr, af_file) && addr->local_part[0] != '/') + g = string_catn(g, CUS"save ", 5); + g = string_get_localpart(addr, g); + } + +/* Other deliveries start with the full address. It we have split it into local +part and domain, use those fields. Some early failures can happen before the +splitting is done; in those cases use the original field. */ + +else + { + uschar * cmp; + int off = g->ptr; /* start of the "full address" */ + + if (addr->local_part) + { + const uschar * s; + g = string_get_localpart(addr, g); + g = string_catn(g, US"@", 1); + s = addr->domain; +#ifdef SUPPORT_I18N + if (testflag(addr, af_utf8_downcvt)) + s = string_localpart_utf8_to_alabel(s, NULL); +#endif + g = string_cat(g, s); + } + else + g = string_cat(g, addr->address); + + /* If the address we are going to print is the same as the top address, + and all parents are not being included, don't add on the top address. First + of all, do a caseless comparison; if this succeeds, do a caseful comparison + on the local parts. */ + + cmp = g->s + off; /* only now, as rebuffer likely done */ + string_from_gstring(g); /* ensure nul-terminated */ + if ( strcmpic(cmp, topaddr->address) == 0 + && Ustrncmp(cmp, topaddr->address, Ustrchr(cmp, '@') - cmp) == 0 + && !addr->onetime_parent + && (!all_parents || !addr->parent || addr->parent == topaddr) + ) + add_topaddr = FALSE; + } + +/* If all parents are requested, or this is a local pipe/file/reply, and +there is at least one intermediate parent, show it in brackets, and continue +with all of them if all are wanted. */ + +if ( (all_parents || testflag(addr, af_pfr)) + && addr->parent + && addr->parent != topaddr) + { + uschar *s = US" ("; + for (address_item * addr2 = addr->parent; addr2 != topaddr; addr2 = addr2->parent) + { + g = string_catn(g, s, 2); + g = string_cat (g, addr2->address); + if (!all_parents) break; + s = US", "; + } + g = string_catn(g, US")", 1); + } + +/* Add the top address if it is required */ + +if (add_topaddr) + g = string_append(g, 3, + US" <", + addr->onetime_parent ? addr->onetime_parent : topaddr->address, + US">"); + +return g; +} + + + +/******************************************************************************/ + + + +/* If msg is NULL this is a delivery log and logchar is used. Otherwise +this is a nonstandard call; no two-character delivery flag is written +but sender-host and sender are prefixed and "msg" is inserted in the log line. + +Arguments: + flags passed to log_write() +*/ +void +delivery_log(int flags, address_item * addr, int logchar, uschar * msg) +{ +gstring * g; /* Used for a temporary, expanding buffer, for building log lines */ +rmark reset_point; + +/* Log the delivery on the main log. We use an extensible string to build up +the log line, and reset the store afterwards. Remote deliveries should always +have a pointer to the host item that succeeded; local deliveries can have a +pointer to a single host item in their host list, for use by the transport. */ + +#ifndef DISABLE_EVENT + /* presume no successful remote delivery */ + lookup_dnssec_authenticated = NULL; +#endif + +reset_point = store_mark(); +g = string_get_tainted(256, GET_TAINTED); /* addrs will be tainted, so avoid copy */ + +if (msg) + g = string_append(g, 2, host_and_ident(TRUE), US" "); +else + { + g->s[0] = logchar; g->ptr = 1; + g = string_catn(g, US"> ", 2); + } +g = string_log_address(g, addr, LOGGING(all_parents), TRUE); + +if (LOGGING(sender_on_delivery) || msg) + g = string_append(g, 3, US" F=<", +#ifdef SUPPORT_I18N + testflag(addr, af_utf8_downcvt) + ? string_address_utf8_to_alabel(sender_address, NULL) + : +#endif + sender_address, + US">"); + +if (*queue_name) + g = string_append(g, 2, US" Q=", queue_name); + +/* You might think that the return path must always be set for a successful +delivery; indeed, I did for some time, until this statement crashed. The case +when it is not set is for a delivery to /dev/null which is optimised by not +being run at all. */ + +if (used_return_path && LOGGING(return_path_on_delivery)) + g = string_append(g, 3, US" P=<", used_return_path, US">"); + +if (msg) + g = string_append(g, 2, US" ", msg); + +/* For a delivery from a system filter, there may not be a router */ +if (addr->router) + g = string_append(g, 2, US" R=", addr->router->name); + +g = string_append(g, 2, US" T=", addr->transport->name); + +if (LOGGING(delivery_size)) + g = string_fmt_append(g, " S=%d", transport_count); + +/* Local delivery */ + +if (addr->transport->info->local) + { + if (addr->host_list) + g = string_append(g, 2, US" H=", addr->host_list->name); + g = d_log_interface(g); + if (addr->shadow_message) + g = string_cat(g, addr->shadow_message); + } + +/* Remote delivery */ + +else + { + if (addr->host_used) + { + g = d_hostlog(g, addr); + +#ifndef DISABLE_EVENT + deliver_host_address = addr->host_used->address; + deliver_host_port = addr->host_used->port; + deliver_host = addr->host_used->name; + + /* DNS lookup status */ + lookup_dnssec_authenticated = addr->host_used->dnssec==DS_YES ? US"yes" + : addr->host_used->dnssec==DS_NO ? US"no" + : NULL; +#endif + } + +#ifndef DISABLE_TLS + g = d_tlslog(g, addr); +#endif + + if (addr->authenticator) + { + g = string_append(g, 2, US" A=", addr->authenticator); + if (addr->auth_id) + { + g = string_append(g, 2, US":", addr->auth_id); + if (LOGGING(smtp_mailauth) && addr->auth_sndr) + g = string_append(g, 2, US":", addr->auth_sndr); + } + } + + if (LOGGING(pipelining)) + { + if (testflag(addr, af_pipelining)) + g = string_catn(g, US" L", 2); +#ifndef DISABLE_PIPE_CONNECT + if (testflag(addr, af_early_pipe)) + g = string_catn(g, US"*", 1); +#endif + } + +#ifndef DISABLE_PRDR + if (testflag(addr, af_prdr_used)) + g = string_catn(g, US" PRDR", 5); +#endif + + if (testflag(addr, af_chunking_used)) + g = string_catn(g, US" K", 2); + } + +/* confirmation message (SMTP (host_used) and LMTP (driver_name)) */ + +if ( LOGGING(smtp_confirmation) + && addr->message + && (addr->host_used || Ustrcmp(addr->transport->driver_name, "lmtp") == 0) + ) + { + unsigned lim = big_buffer_size < 1024 ? big_buffer_size : 1024; + uschar *p = big_buffer; + uschar *ss = addr->message; + *p++ = '\"'; + for (int i = 0; i < lim && ss[i] != 0; i++) /* limit logged amount */ + { + if (ss[i] == '\"' || ss[i] == '\\') *p++ = '\\'; /* quote \ and " */ + *p++ = ss[i]; + } + *p++ = '\"'; + *p = 0; + g = string_append(g, 2, US" C=", big_buffer); + } + +/* Time on queue and actual time taken to deliver */ + +if (LOGGING(queue_time)) + g = string_append(g, 2, US" QT=", string_timesince( + LOGGING(queue_time_exclusive) ? &received_time_complete : &received_time)); + +if (LOGGING(deliver_time)) + g = string_append(g, 2, US" DT=", string_timediff(&addr->delivery_time)); + +/* string_cat() always leaves room for the terminator. Release the +store we used to build the line after writing it. */ + +log_write(0, flags, "%s", string_from_gstring(g)); + +#ifndef DISABLE_EVENT +if (!msg) msg_event_raise(US"msg:delivery", addr); +#endif + +store_reset(reset_point); +return; +} + + + +static void +deferral_log(address_item * addr, uschar * now, + int logflags, uschar * driver_name, uschar * driver_kind) +{ +rmark reset_point = store_mark(); +gstring * g = string_get(256); + +/* Build up the line that is used for both the message log and the main +log. */ + +/* Create the address string for logging. Must not do this earlier, because +an OK result may be changed to FAIL when a pipe returns text. */ + +g = string_log_address(g, addr, LOGGING(all_parents), FALSE); + +if (*queue_name) + g = string_append(g, 2, US" Q=", queue_name); + +/* Either driver_name contains something and driver_kind contains +" router" or " transport" (note the leading space), or driver_name is +a null string and driver_kind contains "routing" without the leading +space, if all routing has been deferred. When a domain has been held, +so nothing has been done at all, both variables contain null strings. */ + +if (driver_name) + { + if (driver_kind[1] == 't' && addr->router) + g = string_append(g, 2, US" R=", addr->router->name); + g = string_fmt_append(g, " %c=%s", toupper(driver_kind[1]), driver_name); + } +else if (driver_kind) + g = string_append(g, 2, US" ", driver_kind); + +g = string_fmt_append(g, " defer (%d)", addr->basic_errno); + +if (addr->basic_errno > 0) + g = string_append(g, 2, US": ", US strerror(addr->basic_errno)); + +if (addr->host_used) + g = d_hostlog(g, addr); + +if (LOGGING(deliver_time)) + g = string_append(g, 2, US" DT=", string_timediff(&addr->delivery_time)); + +if (addr->message) + g = string_append(g, 2, US": ", addr->message); + +(void) string_from_gstring(g); + +/* Log the deferment in the message log, but don't clutter it +up with retry-time defers after the first delivery attempt. */ + +if (f.deliver_firsttime || addr->basic_errno > ERRNO_RETRY_BASE) + deliver_msglog("%s %s\n", now, g->s); + +/* Write the main log and reset the store. +For errors of the type "retry time not reached" (also remotes skipped +on queue run), logging is controlled by L_retry_defer. Note that this kind +of error number is negative, and all the retry ones are less than any +others. */ + + +log_write(addr->basic_errno <= ERRNO_RETRY_BASE ? L_retry_defer : 0, logflags, + "== %s", g->s); + +store_reset(reset_point); +return; +} + + + +static void +failure_log(address_item * addr, uschar * driver_kind, uschar * now) +{ +rmark reset_point = store_mark(); +gstring * g = string_get(256); + +#ifndef DISABLE_EVENT +/* Message failures for which we will send a DSN get their event raised +later so avoid doing it here. */ + +if ( !addr->prop.ignore_error + && !(addr->dsn_flags & (rf_dsnflags & ~rf_notify_failure)) + ) + msg_event_raise(US"msg:fail:delivery", addr); +#endif + +/* Build up the log line for the message and main logs */ + +/* Create the address string for logging. Must not do this earlier, because +an OK result may be changed to FAIL when a pipe returns text. */ + +g = string_log_address(g, addr, LOGGING(all_parents), FALSE); + +if (LOGGING(sender_on_delivery)) + g = string_append(g, 3, US" F=<", sender_address, US">"); + +if (*queue_name) + g = string_append(g, 2, US" Q=", queue_name); + +/* Return path may not be set if no delivery actually happened */ + +if (used_return_path && LOGGING(return_path_on_delivery)) + g = string_append(g, 3, US" P=<", used_return_path, US">"); + +if (addr->router) + g = string_append(g, 2, US" R=", addr->router->name); +if (addr->transport) + g = string_append(g, 2, US" T=", addr->transport->name); + +if (addr->host_used) + g = d_hostlog(g, addr); + +#ifndef DISABLE_TLS +g = d_tlslog(g, addr); +#endif + +if (addr->basic_errno > 0) + g = string_append(g, 2, US": ", US strerror(addr->basic_errno)); + +if (addr->message) + g = string_append(g, 2, US": ", addr->message); + +if (LOGGING(deliver_time)) + g = string_append(g, 2, US" DT=", string_timediff(&addr->delivery_time)); + +(void) string_from_gstring(g); + +/* Do the logging. For the message log, "routing failed" for those cases, +just to make it clearer. */ + +if (driver_kind) + deliver_msglog("%s %s failed for %s\n", now, driver_kind, g->s); +else + deliver_msglog("%s %s\n", now, g->s); + +log_write(0, LOG_MAIN, "** %s", g->s); + +store_reset(reset_point); +return; +} + + + +/************************************************* +* Actions at the end of handling an address * +*************************************************/ + +/* This is a function for processing a single address when all that can be done +with it has been done. + +Arguments: + addr points to the address block + result the result of the delivery attempt + logflags flags for log_write() (LOG_MAIN and/or LOG_PANIC) + driver_type indicates which type of driver (transport, or router) was last + to process the address + logchar '=' or '-' for use when logging deliveries with => or -> + +Returns: nothing +*/ + +static void +post_process_one(address_item *addr, int result, int logflags, int driver_type, + int logchar) +{ +uschar *now = tod_stamp(tod_log); +uschar *driver_kind = NULL; +uschar *driver_name = NULL; + +DEBUG(D_deliver) debug_printf("post-process %s (%d)\n", addr->address, result); + +/* Set up driver kind and name for logging. Disable logging if the router or +transport has disabled it. */ + +if (driver_type == EXIM_DTYPE_TRANSPORT) + { + if (addr->transport) + { + driver_name = addr->transport->name; + driver_kind = US" transport"; + f.disable_logging = addr->transport->disable_logging; + } + else driver_kind = US"transporting"; + } +else if (driver_type == EXIM_DTYPE_ROUTER) + { + if (addr->router) + { + driver_name = addr->router->name; + driver_kind = US" router"; + f.disable_logging = addr->router->disable_logging; + } + else driver_kind = US"routing"; + } + +/* If there's an error message set, ensure that it contains only printing +characters - it should, but occasionally things slip in and this at least +stops the log format from getting wrecked. We also scan the message for an LDAP +expansion item that has a password setting, and flatten the password. This is a +fudge, but I don't know a cleaner way of doing this. (If the item is badly +malformed, it won't ever have gone near LDAP.) */ + +if (addr->message) + { + const uschar * s = string_printing(addr->message); + + /* deconst cast ok as string_printing known to have alloc'n'copied */ + addr->message = expand_hide_passwords(US s); + } + +/* If we used a transport that has one of the "return_output" options set, and +if it did in fact generate some output, then for return_output we treat the +message as failed if it was not already set that way, so that the output gets +returned to the sender, provided there is a sender to send it to. For +return_fail_output, do this only if the delivery failed. Otherwise we just +unlink the file, and remove the name so that if the delivery failed, we don't +try to send back an empty or unwanted file. The log_output options operate only +on a non-empty file. + +In any case, we close the message file, because we cannot afford to leave a +file-descriptor for one address while processing (maybe very many) others. */ + +if (addr->return_file >= 0 && addr->return_filename) + { + BOOL return_output = FALSE; + struct stat statbuf; + (void)EXIMfsync(addr->return_file); + + /* If there is no output, do nothing. */ + + if (fstat(addr->return_file, &statbuf) == 0 && statbuf.st_size > 0) + { + transport_instance *tb = addr->transport; + + /* Handle logging options */ + + if ( tb->log_output + || result == FAIL && tb->log_fail_output + || result == DEFER && tb->log_defer_output + ) + { + uschar *s; + FILE *f = Ufopen(addr->return_filename, "rb"); + if (!f) + log_write(0, LOG_MAIN|LOG_PANIC, "failed to open %s to log output " + "from %s transport: %s", addr->return_filename, tb->name, + strerror(errno)); + else + if ((s = US Ufgets(big_buffer, big_buffer_size, f))) + { + uschar *p = big_buffer + Ustrlen(big_buffer); + const uschar * sp; + while (p > big_buffer && isspace(p[-1])) p--; + *p = 0; + sp = string_printing(big_buffer); + log_write(0, LOG_MAIN, "<%s>: %s transport output: %s", + addr->address, tb->name, sp); + } + (void)fclose(f); + } + + /* Handle returning options, but only if there is an address to return + the text to. */ + + if (sender_address[0] != 0 || addr->prop.errors_address) + if (tb->return_output) + { + addr->transport_return = result = FAIL; + if (addr->basic_errno == 0 && !addr->message) + addr->message = US"return message generated"; + return_output = TRUE; + } + else + if (tb->return_fail_output && result == FAIL) return_output = TRUE; + } + + /* Get rid of the file unless it might be returned, but close it in + all cases. */ + + if (!return_output) + { + Uunlink(addr->return_filename); + addr->return_filename = NULL; + addr->return_file = -1; + } + + (void)close(addr->return_file); + } + +/* Check if the transport notifed continue-conn status explicitly, and +update our knowlege. */ + +if (testflag(addr, af_new_conn)) continue_sequence = 1; +else if (testflag(addr, af_cont_conn)) continue_sequence++; + +/* The success case happens only after delivery by a transport. */ + +if (result == OK) + { + addr->next = addr_succeed; + addr_succeed = addr; + + /* Call address_done() to ensure that we don't deliver to this address again, + and write appropriate things to the message log. If it is a child address, we + call child_done() to scan the ancestors and mark them complete if this is the + last child to complete. */ + + address_done(addr, now); + DEBUG(D_deliver) debug_printf("%s delivered\n", addr->address); + + if (!addr->parent) + deliver_msglog("%s %s: %s%s succeeded\n", now, addr->address, + driver_name, driver_kind); + else + { + deliver_msglog("%s %s <%s>: %s%s succeeded\n", now, addr->address, + addr->parent->address, driver_name, driver_kind); + child_done(addr, now); + } + + /* Certificates for logging (via events) */ +#ifndef DISABLE_TLS + tls_out.ourcert = addr->ourcert; + addr->ourcert = NULL; + tls_out.peercert = addr->peercert; + addr->peercert = NULL; + + tls_out.ver = addr->tlsver; + tls_out.cipher = addr->cipher; + tls_out.peerdn = addr->peerdn; + tls_out.ocsp = addr->ocsp; +# ifdef SUPPORT_DANE + tls_out.dane_verified = testflag(addr, af_dane_verified); +# endif +#endif + + delivery_log(LOG_MAIN, addr, logchar, NULL); + +#ifndef DISABLE_TLS + tls_free_cert(&tls_out.ourcert); + tls_free_cert(&tls_out.peercert); + tls_out.ver = NULL; + tls_out.cipher = NULL; + tls_out.peerdn = NULL; + tls_out.ocsp = OCSP_NOT_REQ; +# ifdef SUPPORT_DANE + tls_out.dane_verified = FALSE; +# endif +#endif + } + + +/* Soft failure, or local delivery process failed; freezing may be +requested. */ + +else if (result == DEFER || result == PANIC) + { + if (result == PANIC) logflags |= LOG_PANIC; + + /* This puts them on the chain in reverse order. Do not change this, because + the code for handling retries assumes that the one with the retry + information is last. */ + + addr->next = addr_defer; + addr_defer = addr; + + /* The only currently implemented special action is to freeze the + message. Logging of this is done later, just before the -H file is + updated. */ + + if (addr->special_action == SPECIAL_FREEZE) + { + f.deliver_freeze = TRUE; + deliver_frozen_at = time(NULL); + update_spool = TRUE; + } + + /* If doing a 2-stage queue run, we skip writing to either the message + log or the main log for SMTP defers. */ + + if (!f.queue_2stage || addr->basic_errno != 0) + deferral_log(addr, now, logflags, driver_name, driver_kind); + } + + +/* Hard failure. If there is an address to which an error message can be sent, +put this address on the failed list. If not, put it on the deferred list and +freeze the mail message for human attention. The latter action can also be +explicitly requested by a router or transport. */ + +else + { + /* If this is a delivery error, or a message for which no replies are + wanted, and the message's age is greater than ignore_bounce_errors_after, + force the af_ignore_error flag. This will cause the address to be discarded + later (with a log entry). */ + + if (!*sender_address && message_age >= ignore_bounce_errors_after) + addr->prop.ignore_error = TRUE; + + /* Freeze the message if requested, or if this is a bounce message (or other + message with null sender) and this address does not have its own errors + address. However, don't freeze if errors are being ignored. The actual code + to ignore occurs later, instead of sending a message. Logging of freezing + occurs later, just before writing the -H file. */ + + if ( !addr->prop.ignore_error + && ( addr->special_action == SPECIAL_FREEZE + || (sender_address[0] == 0 && !addr->prop.errors_address) + ) ) + { + frozen_info = addr->special_action == SPECIAL_FREEZE + ? US"" + : f.sender_local && !f.local_error_message + ? US" (message created with -f <>)" + : US" (delivery error message)"; + f.deliver_freeze = TRUE; + deliver_frozen_at = time(NULL); + update_spool = TRUE; + + /* The address is put on the defer rather than the failed queue, because + the message is being retained. */ + + addr->next = addr_defer; + addr_defer = addr; + } + + /* Don't put the address on the nonrecipients tree yet; wait until an + error message has been successfully sent. */ + + else + { + addr->next = addr_failed; + addr_failed = addr; + } + + failure_log(addr, driver_name ? NULL : driver_kind, now); + } + +/* Ensure logging is turned on again in all cases */ + +f.disable_logging = FALSE; +} + + + + +/************************************************* +* Address-independent error * +*************************************************/ + +/* This function is called when there's an error that is not dependent on a +particular address, such as an expansion string failure. It puts the error into +all the addresses in a batch, logs the incident on the main and panic logs, and +clears the expansions. It is mostly called from local_deliver(), but can be +called for a remote delivery via findugid(). + +Arguments: + logit TRUE if (MAIN+PANIC) logging required + addr the first of the chain of addresses + code the error code + format format string for error message, or NULL if already set in addr + ... arguments for the format + +Returns: nothing +*/ + +static void +common_error(BOOL logit, address_item *addr, int code, uschar *format, ...) +{ +addr->basic_errno = code; + +if (format) + { + va_list ap; + gstring * g; + + va_start(ap, format); + g = string_vformat(NULL, SVFMT_EXTEND|SVFMT_REBUFFER, CS format, ap); + va_end(ap); + addr->message = string_from_gstring(g); + } + +for (address_item * addr2 = addr->next; addr2; addr2 = addr2->next) + { + addr2->basic_errno = code; + addr2->message = addr->message; + } + +if (logit) log_write(0, LOG_MAIN|LOG_PANIC, "%s", addr->message); +deliver_set_expansions(NULL); +} + + + + +/************************************************* +* Check a "never users" list * +*************************************************/ + +/* This function is called to check whether a uid is on one of the two "never +users" lists. + +Arguments: + uid the uid to be checked + nusers the list to be scanned; the first item in the list is the count + +Returns: TRUE if the uid is on the list +*/ + +static BOOL +check_never_users(uid_t uid, uid_t *nusers) +{ +if (!nusers) return FALSE; +for (int i = 1; i <= (int)(nusers[0]); i++) if (nusers[i] == uid) return TRUE; +return FALSE; +} + + + +/************************************************* +* Find uid and gid for a transport * +*************************************************/ + +/* This function is called for both local and remote deliveries, to find the +uid/gid under which to run the delivery. The values are taken preferentially +from the transport (either explicit or deliver_as_creator), then from the +address (i.e. the router), and if nothing is set, the exim uid/gid are used. If +the resulting uid is on the "never_users" or the "fixed_never_users" list, a +panic error is logged, and the function fails (which normally leads to delivery +deferral). + +Arguments: + addr the address (possibly a chain) + tp the transport + uidp pointer to uid field + gidp pointer to gid field + igfp pointer to the use_initgroups field + +Returns: FALSE if failed - error has been set in address(es) +*/ + +static BOOL +findugid(address_item *addr, transport_instance *tp, uid_t *uidp, gid_t *gidp, + BOOL *igfp) +{ +uschar *nuname; +BOOL gid_set = FALSE; + +/* Default initgroups flag comes from the transport */ + +*igfp = tp->initgroups; + +/* First see if there's a gid on the transport, either fixed or expandable. +The expanding function always logs failure itself. */ + +if (tp->gid_set) + { + *gidp = tp->gid; + gid_set = TRUE; + } +else if (tp->expand_gid) + { + if (!route_find_expanded_group(tp->expand_gid, tp->name, US"transport", gidp, + &(addr->message))) + { + common_error(FALSE, addr, ERRNO_GIDFAIL, NULL); + return FALSE; + } + gid_set = TRUE; + } + +/* If the transport did not set a group, see if the router did. */ + +if (!gid_set && testflag(addr, af_gid_set)) + { + *gidp = addr->gid; + gid_set = TRUE; + } + +/* Pick up a uid from the transport if one is set. */ + +if (tp->uid_set) *uidp = tp->uid; + +/* Otherwise, try for an expandable uid field. If it ends up as a numeric id, +it does not provide a passwd value from which a gid can be taken. */ + +else if (tp->expand_uid) + { + struct passwd *pw; + if (!route_find_expanded_user(tp->expand_uid, tp->name, US"transport", &pw, + uidp, &(addr->message))) + { + common_error(FALSE, addr, ERRNO_UIDFAIL, NULL); + return FALSE; + } + if (!gid_set && pw) + { + *gidp = pw->pw_gid; + gid_set = TRUE; + } + } + +/* If the transport doesn't set the uid, test the deliver_as_creator flag. */ + +else if (tp->deliver_as_creator) + { + *uidp = originator_uid; + if (!gid_set) + { + *gidp = originator_gid; + gid_set = TRUE; + } + } + +/* Otherwise see if the address specifies the uid and if so, take it and its +initgroups flag. */ + +else if (testflag(addr, af_uid_set)) + { + *uidp = addr->uid; + *igfp = testflag(addr, af_initgroups); + } + +/* Nothing has specified the uid - default to the Exim user, and group if the +gid is not set. */ + +else + { + *uidp = exim_uid; + if (!gid_set) + { + *gidp = exim_gid; + gid_set = TRUE; + } + } + +/* If no gid is set, it is a disaster. We default to the Exim gid only if +defaulting to the Exim uid. In other words, if the configuration has specified +a uid, it must also provide a gid. */ + +if (!gid_set) + { + common_error(TRUE, addr, ERRNO_GIDFAIL, US"User set without group for " + "%s transport", tp->name); + return FALSE; + } + +/* Check that the uid is not on the lists of banned uids that may not be used +for delivery processes. */ + +nuname = check_never_users(*uidp, never_users) + ? US"never_users" + : check_never_users(*uidp, fixed_never_users) + ? US"fixed_never_users" + : NULL; +if (nuname) + { + common_error(TRUE, addr, ERRNO_UIDFAIL, US"User %ld set for %s transport " + "is on the %s list", (long int)(*uidp), tp->name, nuname); + return FALSE; + } + +/* All is well */ + +return TRUE; +} + + + + +/************************************************* +* Check the size of a message for a transport * +*************************************************/ + +/* Checks that the message isn't too big for the selected transport. +This is called only when it is known that the limit is set. + +Arguments: + tp the transport + addr the (first) address being delivered + +Returns: OK + DEFER expansion failed or did not yield an integer + FAIL message too big +*/ + +int +check_message_size(transport_instance *tp, address_item *addr) +{ +int rc = OK; +int size_limit; + +deliver_set_expansions(addr); +size_limit = expand_string_integer(tp->message_size_limit, TRUE); +deliver_set_expansions(NULL); + +if (expand_string_message) + { + rc = DEFER; + addr->message = size_limit == -1 + ? string_sprintf("failed to expand message_size_limit " + "in %s transport: %s", tp->name, expand_string_message) + : string_sprintf("invalid message_size_limit " + "in %s transport: %s", tp->name, expand_string_message); + } +else if (size_limit > 0 && message_size > size_limit) + { + rc = FAIL; + addr->message = + string_sprintf("message is too big (transport limit = %d)", + size_limit); + } + +return rc; +} + + + +/************************************************* +* Transport-time check for a previous delivery * +*************************************************/ + +/* Check that this base address hasn't previously been delivered to its routed +transport. If it has been delivered, mark it done. The check is necessary at +delivery time in order to handle homonymic addresses correctly in cases where +the pattern of redirection changes between delivery attempts (so the unique +fields change). Non-homonymic previous delivery is detected earlier, at routing +time (which saves unnecessary routing). + +Arguments: + addr the address item + testing TRUE if testing wanted only, without side effects + +Returns: TRUE if previously delivered by the transport +*/ + +static BOOL +previously_transported(address_item *addr, BOOL testing) +{ +uschar * s = string_sprintf("%s/%s", + addr->unique + (testflag(addr, af_homonym)? 3:0), addr->transport->name); + +if (tree_search(tree_nonrecipients, s) != 0) + { + DEBUG(D_deliver|D_route|D_transport) + debug_printf("%s was previously delivered (%s transport): discarded\n", + addr->address, addr->transport->name); + if (!testing) child_done(addr, tod_stamp(tod_log)); + return TRUE; + } + +return FALSE; +} + + + +/****************************************************** +* Check for a given header in a header string * +******************************************************/ + +/* This function is used when generating quota warnings. The configuration may +specify any header lines it likes in quota_warn_message. If certain of them are +missing, defaults are inserted, so we need to be able to test for the presence +of a given header. + +Arguments: + hdr the required header name + hstring the header string + +Returns: TRUE the header is in the string + FALSE the header is not in the string +*/ + +static BOOL +contains_header(uschar *hdr, uschar *hstring) +{ +int len = Ustrlen(hdr); +uschar *p = hstring; +while (*p != 0) + { + if (strncmpic(p, hdr, len) == 0) + { + p += len; + while (*p == ' ' || *p == '\t') p++; + if (*p == ':') return TRUE; + } + while (*p != 0 && *p != '\n') p++; + if (*p == '\n') p++; + } +return FALSE; +} + + + + +/************************************************* +* Perform a local delivery * +*************************************************/ + +/* Each local delivery is performed in a separate process which sets its +uid and gid as specified. This is a safer way than simply changing and +restoring using seteuid(); there is a body of opinion that seteuid() +cannot be used safely. From release 4, Exim no longer makes any use of +it for delivery. Besides, not all systems have seteuid(). + +If the uid/gid are specified in the transport_instance, they are used; the +transport initialization must ensure that either both or neither are set. +Otherwise, the values associated with the address are used. If neither are set, +it is a configuration error. + +The transport or the address may specify a home directory (transport over- +rides), and if they do, this is set as $home. If neither have set a working +directory, this value is used for that as well. Otherwise $home is left unset +and the cwd is set to "/" - a directory that should be accessible to all users. + +Using a separate process makes it more complicated to get error information +back. We use a pipe to pass the return code and also an error code and error +text string back to the parent process. + +Arguments: + addr points to an address block for this delivery; for "normal" local + deliveries this is the only address to be delivered, but for + pseudo-remote deliveries (e.g. by batch SMTP to a file or pipe) + a number of addresses can be handled simultaneously, and in this + case addr will point to a chain of addresses with the same + characteristics. + + shadowing TRUE if running a shadow transport; this causes output from pipes + to be ignored. + +Returns: nothing +*/ + +void +deliver_local(address_item *addr, BOOL shadowing) +{ +BOOL use_initgroups; +uid_t uid; +gid_t gid; +int status, len, rc; +int pfd[2]; +pid_t pid; +uschar *working_directory; +address_item *addr2; +transport_instance *tp = addr->transport; + +/* Set up the return path from the errors or sender address. If the transport +has its own return path setting, expand it and replace the existing value. */ + +if(addr->prop.errors_address) + return_path = addr->prop.errors_address; +else + return_path = sender_address; + +if (tp->return_path) + { + uschar * new_return_path = expand_string(tp->return_path); + if (new_return_path) + return_path = new_return_path; + else if (!f.expand_string_forcedfail) + { + common_error(TRUE, addr, ERRNO_EXPANDFAIL, + US"Failed to expand return path \"%s\" in %s transport: %s", + tp->return_path, tp->name, expand_string_message); + return; + } + } + +/* For local deliveries, one at a time, the value used for logging can just be +set directly, once and for all. */ + +used_return_path = return_path; + +/* Sort out the uid, gid, and initgroups flag. If an error occurs, the message +gets put into the address(es), and the expansions are unset, so we can just +return. */ + +if (!findugid(addr, tp, &uid, &gid, &use_initgroups)) return; + +/* See if either the transport or the address specifies a home directory. A +home directory set in the address may already be expanded; a flag is set to +indicate that. In other cases we must expand it. */ + +if ( (deliver_home = tp->home_dir) /* Set in transport, or */ + || ( (deliver_home = addr->home_dir) /* Set in address and */ + && !testflag(addr, af_home_expanded) /* not expanded */ + ) ) + { + uschar *rawhome = deliver_home; + deliver_home = NULL; /* in case it contains $home */ + if (!(deliver_home = expand_string(rawhome))) + { + common_error(TRUE, addr, ERRNO_EXPANDFAIL, US"home directory \"%s\" failed " + "to expand for %s transport: %s", rawhome, tp->name, + expand_string_message); + return; + } + if (*deliver_home != '/') + { + common_error(TRUE, addr, ERRNO_NOTABSOLUTE, US"home directory path \"%s\" " + "is not absolute for %s transport", deliver_home, tp->name); + return; + } + } + +/* See if either the transport or the address specifies a current directory, +and if so, expand it. If nothing is set, use the home directory, unless it is +also unset in which case use "/", which is assumed to be a directory to which +all users have access. It is necessary to be in a visible directory for some +operating systems when running pipes, as some commands (e.g. "rm" under Solaris +2.5) require this. */ + +working_directory = tp->current_dir ? tp->current_dir : addr->current_dir; +if (working_directory) + { + uschar *raw = working_directory; + if (!(working_directory = expand_string(raw))) + { + common_error(TRUE, addr, ERRNO_EXPANDFAIL, US"current directory \"%s\" " + "failed to expand for %s transport: %s", raw, tp->name, + expand_string_message); + return; + } + if (*working_directory != '/') + { + common_error(TRUE, addr, ERRNO_NOTABSOLUTE, US"current directory path " + "\"%s\" is not absolute for %s transport", working_directory, tp->name); + return; + } + } +else working_directory = deliver_home ? deliver_home : US"/"; + +/* If one of the return_output flags is set on the transport, create and open a +file in the message log directory for the transport to write its output onto. +This is mainly used by pipe transports. The file needs to be unique to the +address. This feature is not available for shadow transports. */ + +if ( !shadowing + && ( tp->return_output || tp->return_fail_output + || tp->log_output || tp->log_fail_output || tp->log_defer_output + ) ) + { + uschar * error; + + addr->return_filename = + spool_fname(US"msglog", message_subdir, message_id, + string_sprintf("-%d-%d", getpid(), return_count++)); + + if ((addr->return_file = open_msglog_file(addr->return_filename, 0400, &error)) < 0) + { + common_error(TRUE, addr, errno, US"Unable to %s file for %s transport " + "to return message: %s", error, tp->name, strerror(errno)); + return; + } + } + +/* Create the pipe for inter-process communication. */ + +if (pipe(pfd) != 0) + { + common_error(TRUE, addr, ERRNO_PIPEFAIL, US"Creation of pipe failed: %s", + strerror(errno)); + return; + } + +/* Now fork the process to do the real work in the subprocess, but first +ensure that all cached resources are freed so that the subprocess starts with +a clean slate and doesn't interfere with the parent process. */ + +search_tidyup(); + +if ((pid = exim_fork(US"delivery-local")) == 0) + { + BOOL replicate = TRUE; + + /* Prevent core dumps, as we don't want them in users' home directories. + HP-UX doesn't have RLIMIT_CORE; I don't know how to do this in that + system. Some experimental/developing systems (e.g. GNU/Hurd) may define + RLIMIT_CORE but not support it in setrlimit(). For such systems, do not + complain if the error is "not supported". + + There are two scenarios where changing the max limit has an effect. In one, + the user is using a .forward and invoking a command of their choice via pipe; + for these, we do need the max limit to be 0 unless the admin chooses to + permit an increased limit. In the other, the command is invoked directly by + the transport and is under administrator control, thus being able to raise + the limit aids in debugging. So there's no general always-right answer. + + Thus we inhibit core-dumps completely but let individual transports, while + still root, re-raise the limits back up to aid debugging. We make the + default be no core-dumps -- few enough people can use core dumps in + diagnosis that it's reasonable to make them something that has to be explicitly requested. + */ + +#ifdef RLIMIT_CORE + struct rlimit rl; + rl.rlim_cur = 0; + rl.rlim_max = 0; + if (setrlimit(RLIMIT_CORE, &rl) < 0) + { +# ifdef SETRLIMIT_NOT_SUPPORTED + if (errno != ENOSYS && errno != ENOTSUP) +# endif + log_write(0, LOG_MAIN|LOG_PANIC, "setrlimit(RLIMIT_CORE) failed: %s", + strerror(errno)); + } +#endif + + /* Reset the random number generator, so different processes don't all + have the same sequence. */ + + random_seed = 0; + + /* If the transport has a setup entry, call this first, while still + privileged. (Appendfile uses this to expand quota, for example, while + able to read private files.) */ + + if (addr->transport->setup) + switch((addr->transport->setup)(addr->transport, addr, NULL, uid, gid, + &(addr->message))) + { + case DEFER: + addr->transport_return = DEFER; + goto PASS_BACK; + + case FAIL: + addr->transport_return = PANIC; + goto PASS_BACK; + } + + /* Ignore SIGINT and SIGTERM during delivery. Also ignore SIGUSR1, as + when the process becomes unprivileged, it won't be able to write to the + process log. SIGHUP is ignored throughout exim, except when it is being + run as a daemon. */ + + signal(SIGINT, SIG_IGN); + signal(SIGTERM, SIG_IGN); + signal(SIGUSR1, SIG_IGN); + + /* Close the unwanted half of the pipe, and set close-on-exec for the other + half - for transports that exec things (e.g. pipe). Then set the required + gid/uid. */ + + (void)close(pfd[pipe_read]); + (void)fcntl(pfd[pipe_write], F_SETFD, fcntl(pfd[pipe_write], F_GETFD) | + FD_CLOEXEC); + exim_setugid(uid, gid, use_initgroups, + string_sprintf("local delivery to %s <%s> transport=%s", addr->local_part, + addr->address, addr->transport->name)); + + DEBUG(D_deliver) + { + debug_printf(" home=%s current=%s\n", deliver_home, working_directory); + for (address_item * batched = addr->next; batched; batched = batched->next) + debug_printf("additional batched address: %s\n", batched->address); + } + + /* Set an appropriate working directory. */ + + if (Uchdir(working_directory) < 0) + { + addr->transport_return = DEFER; + addr->basic_errno = errno; + addr->message = string_sprintf("failed to chdir to %s", working_directory); + } + + /* If successful, call the transport */ + + else + { + BOOL ok = TRUE; + set_process_info("delivering %s to %s using %s", message_id, + addr->local_part, tp->name); + + /* Setting these globals in the subprocess means we need never clear them */ + transport_name = addr->transport->name; + driver_srcfile = tp->srcfile; + driver_srcline = tp->srcline; + + /* If a transport filter has been specified, set up its argument list. + Any errors will get put into the address, and FALSE yielded. */ + + if (tp->filter_command) + { + ok = transport_set_up_command(&transport_filter_argv, + tp->filter_command, + TRUE, PANIC, addr, FALSE, US"transport filter", NULL); + transport_filter_timeout = tp->filter_timeout; + } + else transport_filter_argv = NULL; + + if (ok) + { + debug_print_string(tp->debug_string); + replicate = !(tp->info->code)(addr->transport, addr); + } + } + + /* Pass the results back down the pipe. If necessary, first replicate the + status in the top address to the others in the batch. The label is the + subject of a goto when a call to the transport's setup function fails. We + pass the pointer to the transport back in case it got changed as a result of + file_format in appendfile. */ + + PASS_BACK: + + if (replicate) replicate_status(addr); + for (addr2 = addr; addr2; addr2 = addr2->next) + { + int i; + int local_part_length = Ustrlen(addr2->local_part); + uschar *s; + int ret; + + if( (i = addr2->transport_return, (ret = write(pfd[pipe_write], &i, sizeof(int))) != sizeof(int)) + || (ret = write(pfd[pipe_write], &transport_count, sizeof(transport_count))) != sizeof(transport_count) + || (ret = write(pfd[pipe_write], &addr2->flags, sizeof(addr2->flags))) != sizeof(addr2->flags) + || (ret = write(pfd[pipe_write], &addr2->basic_errno, sizeof(int))) != sizeof(int) + || (ret = write(pfd[pipe_write], &addr2->more_errno, sizeof(int))) != sizeof(int) + || (ret = write(pfd[pipe_write], &addr2->delivery_time, sizeof(struct timeval))) != sizeof(struct timeval) + || (i = addr2->special_action, (ret = write(pfd[pipe_write], &i, sizeof(int))) != sizeof(int)) + || (ret = write(pfd[pipe_write], &addr2->transport, + sizeof(transport_instance *))) != sizeof(transport_instance *) + + /* For a file delivery, pass back the local part, in case the original + was only part of the final delivery path. This gives more complete + logging. */ + + || (testflag(addr2, af_file) + && ( (ret = write(pfd[pipe_write], &local_part_length, sizeof(int))) != sizeof(int) + || (ret = write(pfd[pipe_write], addr2->local_part, local_part_length)) != local_part_length + ) + ) + ) + log_write(0, LOG_MAIN|LOG_PANIC, "Failed writing transport results to pipe: %s", + ret == -1 ? strerror(errno) : "short write"); + + /* Now any messages */ + + for (i = 0, s = addr2->message; i < 2; i++, s = addr2->user_message) + { + int message_length = s ? Ustrlen(s) + 1 : 0; + if( (ret = write(pfd[pipe_write], &message_length, sizeof(int))) != sizeof(int) + || message_length > 0 && (ret = write(pfd[pipe_write], s, message_length)) != message_length + ) + log_write(0, LOG_MAIN|LOG_PANIC, "Failed writing transport results to pipe: %s", + ret == -1 ? strerror(errno) : "short write"); + } + } + + /* OK, this process is now done. Free any cached resources that it opened, + and close the pipe we were writing down before exiting. */ + + (void)close(pfd[pipe_write]); + search_tidyup(); + exit(EXIT_SUCCESS); + } + +/* Back in the main process: panic if the fork did not succeed. This seems +better than returning an error - if forking is failing it is probably best +not to try other deliveries for this message. */ + +if (pid < 0) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "Fork failed for local delivery to %s", + addr->address); + +/* Read the pipe to get the delivery status codes and error messages. Our copy +of the writing end must be closed first, as otherwise read() won't return zero +on an empty pipe. We check that a status exists for each address before +overwriting the address structure. If data is missing, the default DEFER status +will remain. Afterwards, close the reading end. */ + +(void)close(pfd[pipe_write]); + +for (addr2 = addr; addr2; addr2 = addr2->next) + { + if ((len = read(pfd[pipe_read], &status, sizeof(int))) > 0) + { + int i; + uschar **sptr; + + addr2->transport_return = status; + len = read(pfd[pipe_read], &transport_count, + sizeof(transport_count)); + len = read(pfd[pipe_read], &addr2->flags, sizeof(addr2->flags)); + len = read(pfd[pipe_read], &addr2->basic_errno, sizeof(int)); + len = read(pfd[pipe_read], &addr2->more_errno, sizeof(int)); + len = read(pfd[pipe_read], &addr2->delivery_time, sizeof(struct timeval)); + len = read(pfd[pipe_read], &i, sizeof(int)); addr2->special_action = i; + len = read(pfd[pipe_read], &addr2->transport, + sizeof(transport_instance *)); + + if (testflag(addr2, af_file)) + { + int llen; + if ( read(pfd[pipe_read], &llen, sizeof(int)) != sizeof(int) + || llen > 64*4 /* limit from rfc 5821, times I18N factor */ + ) + { + log_write(0, LOG_MAIN|LOG_PANIC, "bad local_part length read" + " from delivery subprocess"); + break; + } + /* sanity-checked llen so disable the Coverity error */ + /* coverity[tainted_data] */ + if (read(pfd[pipe_read], big_buffer, llen) != llen) + { + log_write(0, LOG_MAIN|LOG_PANIC, "bad local_part read" + " from delivery subprocess"); + break; + } + big_buffer[llen] = 0; + addr2->local_part = string_copy(big_buffer); + } + + for (i = 0, sptr = &addr2->message; i < 2; i++, sptr = &addr2->user_message) + { + int message_length; + len = read(pfd[pipe_read], &message_length, sizeof(int)); + if (message_length > 0) + { + len = read(pfd[pipe_read], big_buffer, message_length); + big_buffer[big_buffer_size-1] = '\0'; /* guard byte */ + if (len > 0) *sptr = string_copy(big_buffer); + } + } + } + + else + { + log_write(0, LOG_MAIN|LOG_PANIC, "failed to read delivery status for %s " + "from delivery subprocess", addr2->unique); + break; + } + } + +(void)close(pfd[pipe_read]); + +/* Unless shadowing, write all successful addresses immediately to the journal +file, to ensure they are recorded asap. For homonymic addresses, use the base +address plus the transport name. Failure to write the journal is panic-worthy, +but don't stop, as it may prove possible subsequently to update the spool file +in order to record the delivery. */ + +if (!shadowing) + { + for (addr2 = addr; addr2; addr2 = addr2->next) + if (addr2->transport_return == OK) + { + if (testflag(addr2, af_homonym)) + sprintf(CS big_buffer, "%.500s/%s\n", addr2->unique + 3, tp->name); + else + sprintf(CS big_buffer, "%.500s\n", addr2->unique); + + /* In the test harness, wait just a bit to let the subprocess finish off + any debug output etc first. */ + + testharness_pause_ms(300); + + DEBUG(D_deliver) debug_printf("journalling %s", big_buffer); + len = Ustrlen(big_buffer); + if (write(journal_fd, big_buffer, len) != len) + log_write(0, LOG_MAIN|LOG_PANIC, "failed to update journal for %s: %s", + big_buffer, strerror(errno)); + } + + /* Ensure the journal file is pushed out to disk. */ + + if (EXIMfsync(journal_fd) < 0) + log_write(0, LOG_MAIN|LOG_PANIC, "failed to fsync journal: %s", + strerror(errno)); + } + +/* Wait for the process to finish. If it terminates with a non-zero code, +freeze the message (except for SIGTERM, SIGKILL and SIGQUIT), but leave the +status values of all the addresses as they are. Take care to handle the case +when the subprocess doesn't seem to exist. This has been seen on one system +when Exim was called from an MUA that set SIGCHLD to SIG_IGN. When that +happens, wait() doesn't recognize the termination of child processes. Exim now +resets SIGCHLD to SIG_DFL, but this code should still be robust. */ + +while ((rc = wait(&status)) != pid) + if (rc < 0 && errno == ECHILD) /* Process has vanished */ + { + log_write(0, LOG_MAIN, "%s transport process vanished unexpectedly", + addr->transport->driver_name); + status = 0; + break; + } + +if ((status & 0xffff) != 0) + { + int msb = (status >> 8) & 255; + int lsb = status & 255; + int code = (msb == 0)? (lsb & 0x7f) : msb; + if (msb != 0 || (code != SIGTERM && code != SIGKILL && code != SIGQUIT)) + addr->special_action = SPECIAL_FREEZE; + log_write(0, LOG_MAIN|LOG_PANIC, "%s transport process returned non-zero " + "status 0x%04x: %s %d", + addr->transport->driver_name, + status, + msb == 0 ? "terminated by signal" : "exit code", + code); + } + +/* If SPECIAL_WARN is set in the top address, send a warning message. */ + +if (addr->special_action == SPECIAL_WARN && addr->transport->warn_message) + { + int fd; + uschar *warn_message; + pid_t pid; + + DEBUG(D_deliver) debug_printf("Warning message requested by transport\n"); + + if (!(warn_message = expand_string(addr->transport->warn_message))) + log_write(0, LOG_MAIN|LOG_PANIC, "Failed to expand \"%s\" (warning " + "message for %s transport): %s", addr->transport->warn_message, + addr->transport->name, expand_string_message); + + else if ((pid = child_open_exim(&fd, US"tpt-warning-message")) > 0) + { + FILE *f = fdopen(fd, "wb"); + if (errors_reply_to && !contains_header(US"Reply-To", warn_message)) + fprintf(f, "Reply-To: %s\n", errors_reply_to); + fprintf(f, "Auto-Submitted: auto-replied\n"); + if (!contains_header(US"From", warn_message)) + moan_write_from(f); + fprintf(f, "%s", CS warn_message); + + /* Close and wait for child process to complete, without a timeout. */ + + (void)fclose(f); + (void)child_close(pid, 0); + } + + addr->special_action = SPECIAL_NONE; + } +} + + + + +/* Check transport for the given concurrency limit. Return TRUE if over +the limit (or an expansion failure), else FALSE and if there was a limit, +the key for the hints database used for the concurrency count. */ + +static BOOL +tpt_parallel_check(transport_instance * tp, address_item * addr, uschar ** key) +{ +unsigned max_parallel; + +if (!tp->max_parallel) return FALSE; + +max_parallel = (unsigned) expand_string_integer(tp->max_parallel, TRUE); +if (expand_string_message) + { + log_write(0, LOG_MAIN|LOG_PANIC, "Failed to expand max_parallel option " + "in %s transport (%s): %s", tp->name, addr->address, + expand_string_message); + return TRUE; + } + +if (max_parallel > 0) + { + uschar * serialize_key = string_sprintf("tpt-serialize-%s", tp->name); + if (!enq_start(serialize_key, max_parallel)) + { + address_item * next; + DEBUG(D_transport) + debug_printf("skipping tpt %s because concurrency limit %u reached\n", + tp->name, max_parallel); + do + { + next = addr->next; + addr->message = US"concurrency limit reached for transport"; + addr->basic_errno = ERRNO_TRETRY; + post_process_one(addr, DEFER, LOG_MAIN, EXIM_DTYPE_TRANSPORT, 0); + } while ((addr = next)); + return TRUE; + } + *key = serialize_key; + } +return FALSE; +} + + + +/************************************************* +* Do local deliveries * +*************************************************/ + +/* This function processes the list of addresses in addr_local. True local +deliveries are always done one address at a time. However, local deliveries can +be batched up in some cases. Typically this is when writing batched SMTP output +files for use by some external transport mechanism, or when running local +deliveries over LMTP. + +Arguments: None +Returns: Nothing +*/ + +static void +do_local_deliveries(void) +{ +open_db dbblock; +open_db *dbm_file = NULL; +time_t now = time(NULL); + +/* Loop until we have exhausted the supply of local deliveries */ + +while (addr_local) + { + struct timeval delivery_start; + struct timeval deliver_time; + address_item *addr2, *addr3, *nextaddr; + int logflags = LOG_MAIN; + int logchar = f.dont_deliver? '*' : '='; + transport_instance *tp; + uschar * serialize_key = NULL; + + /* Pick the first undelivered address off the chain */ + + address_item *addr = addr_local; + addr_local = addr->next; + addr->next = NULL; + + DEBUG(D_deliver|D_transport) + debug_printf("--------> %s <--------\n", addr->address); + + /* An internal disaster if there is no transport. Should not occur! */ + + if (!(tp = addr->transport)) + { + logflags |= LOG_PANIC; + f.disable_logging = FALSE; /* Jic */ + addr->message = addr->router + ? string_sprintf("No transport set by %s router", addr->router->name) + : US"No transport set by system filter"; + post_process_one(addr, DEFER, logflags, EXIM_DTYPE_TRANSPORT, 0); + continue; + } + + /* Check that this base address hasn't previously been delivered to this + transport. The check is necessary at this point to handle homonymic addresses + correctly in cases where the pattern of redirection changes between delivery + attempts. Non-homonymic previous delivery is detected earlier, at routing + time. */ + + if (previously_transported(addr, FALSE)) continue; + + /* There are weird cases where logging is disabled */ + + f.disable_logging = tp->disable_logging; + + /* Check for batched addresses and possible amalgamation. Skip all the work + if either batch_max <= 1 or there aren't any other addresses for local + delivery. */ + + if (tp->batch_max > 1 && addr_local) + { + int batch_count = 1; + BOOL uses_dom = readconf_depends((driver_instance *)tp, US"domain"); + BOOL uses_lp = ( testflag(addr, af_pfr) + && (testflag(addr, af_file) || addr->local_part[0] == '|') + ) + || readconf_depends((driver_instance *)tp, US"local_part"); + uschar *batch_id = NULL; + address_item **anchor = &addr_local; + address_item *last = addr; + address_item *next; + + /* Expand the batch_id string for comparison with other addresses. + Expansion failure suppresses batching. */ + + if (tp->batch_id) + { + deliver_set_expansions(addr); + batch_id = expand_string(tp->batch_id); + deliver_set_expansions(NULL); + if (!batch_id) + { + log_write(0, LOG_MAIN|LOG_PANIC, "Failed to expand batch_id option " + "in %s transport (%s): %s", tp->name, addr->address, + expand_string_message); + batch_count = tp->batch_max; + } + } + + /* Until we reach the batch_max limit, pick off addresses which have the + same characteristics. These are: + + same transport + not previously delivered (see comment about 50 lines above) + same local part if the transport's configuration contains $local_part + or if this is a file or pipe delivery from a redirection + same domain if the transport's configuration contains $domain + same errors address + same additional headers + same headers to be removed + same uid/gid for running the transport + same first host if a host list is set + */ + + while ((next = *anchor) && batch_count < tp->batch_max) + { + BOOL ok = + tp == next->transport + && !previously_transported(next, TRUE) + && testflag(addr, af_pfr) == testflag(next, af_pfr) + && testflag(addr, af_file) == testflag(next, af_file) + && (!uses_lp || Ustrcmp(next->local_part, addr->local_part) == 0) + && (!uses_dom || Ustrcmp(next->domain, addr->domain) == 0) + && same_strings(next->prop.errors_address, addr->prop.errors_address) + && same_headers(next->prop.extra_headers, addr->prop.extra_headers) + && same_strings(next->prop.remove_headers, addr->prop.remove_headers) + && same_ugid(tp, addr, next) + && ( !addr->host_list && !next->host_list + || addr->host_list + && next->host_list + && Ustrcmp(addr->host_list->name, next->host_list->name) == 0 + ); + + /* If the transport has a batch_id setting, batch_id will be non-NULL + from the expansion outside the loop. Expand for this address and compare. + Expansion failure makes this address ineligible for batching. */ + + if (ok && batch_id) + { + uschar *bid; + address_item *save_nextnext = next->next; + next->next = NULL; /* Expansion for a single address */ + deliver_set_expansions(next); + next->next = save_nextnext; + bid = expand_string(tp->batch_id); + deliver_set_expansions(NULL); + if (!bid) + { + log_write(0, LOG_MAIN|LOG_PANIC, "Failed to expand batch_id option " + "in %s transport (%s): %s", tp->name, next->address, + expand_string_message); + ok = FALSE; + } + else ok = (Ustrcmp(batch_id, bid) == 0); + } + + /* Take address into batch if OK. */ + + if (ok) + { + *anchor = next->next; /* Include the address */ + next->next = NULL; + last->next = next; + last = next; + batch_count++; + } + else anchor = &next->next; /* Skip the address */ + } + } + + /* We now have one or more addresses that can be delivered in a batch. Check + whether the transport is prepared to accept a message of this size. If not, + fail them all forthwith. If the expansion fails, or does not yield an + integer, defer delivery. */ + + if (tp->message_size_limit) + { + int rc = check_message_size(tp, addr); + if (rc != OK) + { + replicate_status(addr); + while (addr) + { + addr2 = addr->next; + post_process_one(addr, rc, logflags, EXIM_DTYPE_TRANSPORT, 0); + addr = addr2; + } + continue; /* With next batch of addresses */ + } + } + + /* If we are not running the queue, or if forcing, all deliveries will be + attempted. Otherwise, we must respect the retry times for each address. Even + when not doing this, we need to set up the retry key string, and determine + whether a retry record exists, because after a successful delivery, a delete + retry item must be set up. Keep the retry database open only for the duration + of these checks, rather than for all local deliveries, because some local + deliveries (e.g. to pipes) can take a substantial time. */ + + if (!(dbm_file = dbfn_open(US"retry", O_RDONLY, &dbblock, FALSE, TRUE))) + DEBUG(D_deliver|D_retry|D_hints_lookup) + debug_printf("no retry data available\n"); + + addr2 = addr; + addr3 = NULL; + while (addr2) + { + BOOL ok = TRUE; /* to deliver this address */ + uschar *retry_key; + + /* Set up the retry key to include the domain or not, and change its + leading character from "R" to "T". Must make a copy before doing this, + because the old key may be pointed to from a "delete" retry item after + a routing delay. */ + + retry_key = string_copy( + tp->retry_use_local_part ? addr2->address_retry_key : + addr2->domain_retry_key); + *retry_key = 'T'; + + /* Inspect the retry data. If there is no hints file, delivery happens. */ + + if (dbm_file) + { + dbdata_retry *retry_record = dbfn_read(dbm_file, retry_key); + + /* If there is no retry record, delivery happens. If there is, + remember it exists so it can be deleted after a successful delivery. */ + + if (retry_record) + { + setflag(addr2, af_lt_retry_exists); + + /* A retry record exists for this address. If queue running and not + forcing, inspect its contents. If the record is too old, or if its + retry time has come, or if it has passed its cutoff time, delivery + will go ahead. */ + + DEBUG(D_retry) + { + debug_printf("retry record exists: age=%s ", + readconf_printtime(now - retry_record->time_stamp)); + debug_printf("(max %s)\n", readconf_printtime(retry_data_expire)); + debug_printf(" time to retry = %s expired = %d\n", + readconf_printtime(retry_record->next_try - now), + retry_record->expired); + } + + if (f.queue_running && !f.deliver_force) + { + ok = (now - retry_record->time_stamp > retry_data_expire) + || (now >= retry_record->next_try) + || retry_record->expired; + + /* If we haven't reached the retry time, there is one more check + to do, which is for the ultimate address timeout. */ + + if (!ok) + ok = retry_ultimate_address_timeout(retry_key, addr2->domain, + retry_record, now); + } + } + else DEBUG(D_retry) debug_printf("no retry record exists\n"); + } + + /* This address is to be delivered. Leave it on the chain. */ + + if (ok) + { + addr3 = addr2; + addr2 = addr2->next; + } + + /* This address is to be deferred. Take it out of the chain, and + post-process it as complete. Must take it out of the chain first, + because post processing puts it on another chain. */ + + else + { + address_item *this = addr2; + this->message = US"Retry time not yet reached"; + this->basic_errno = ERRNO_LRETRY; + addr2 = addr3 ? (addr3->next = addr2->next) + : (addr = addr2->next); + post_process_one(this, DEFER, logflags, EXIM_DTYPE_TRANSPORT, 0); + } + } + + if (dbm_file) dbfn_close(dbm_file); + + /* If there are no addresses left on the chain, they all deferred. Loop + for the next set of addresses. */ + + if (!addr) continue; + + /* If the transport is limited for parallellism, enforce that here. + We use a hints DB entry, incremented here and decremented after + the transport (and any shadow transport) completes. */ + + if (tpt_parallel_check(tp, addr, &serialize_key)) + { + if (expand_string_message) + { + logflags |= LOG_PANIC; + do + { + addr = addr->next; + post_process_one(addr, DEFER, logflags, EXIM_DTYPE_TRANSPORT, 0); + } while ((addr = addr2)); + } + continue; /* Loop for the next set of addresses. */ + } + + + /* So, finally, we do have some addresses that can be passed to the + transport. Before doing so, set up variables that are relevant to a + single delivery. */ + + deliver_set_expansions(addr); + + gettimeofday(&delivery_start, NULL); + deliver_local(addr, FALSE); + timesince(&deliver_time, &delivery_start); + + /* If a shadow transport (which must perforce be another local transport), is + defined, and its condition is met, we must pass the message to the shadow + too, but only those addresses that succeeded. We do this by making a new + chain of addresses - also to keep the original chain uncontaminated. We must + use a chain rather than doing it one by one, because the shadow transport may + batch. + + NOTE: if the condition fails because of a lookup defer, there is nothing we + can do! */ + + if ( tp->shadow + && ( !tp->shadow_condition + || expand_check_condition(tp->shadow_condition, tp->name, US"transport") + ) ) + { + transport_instance *stp; + address_item *shadow_addr = NULL; + address_item **last = &shadow_addr; + + for (stp = transports; stp; stp = stp->next) + if (Ustrcmp(stp->name, tp->shadow) == 0) break; + + if (!stp) + log_write(0, LOG_MAIN|LOG_PANIC, "shadow transport \"%s\" not found ", + tp->shadow); + + /* Pick off the addresses that have succeeded, and make clones. Put into + the shadow_message field a pointer to the shadow_message field of the real + address. */ + + else for (addr2 = addr; addr2; addr2 = addr2->next) + if (addr2->transport_return == OK) + { + addr3 = store_get(sizeof(address_item), GET_UNTAINTED); + *addr3 = *addr2; + addr3->next = NULL; + addr3->shadow_message = US &addr2->shadow_message; + addr3->transport = stp; + addr3->transport_return = DEFER; + addr3->return_filename = NULL; + addr3->return_file = -1; + *last = addr3; + last = &addr3->next; + } + + /* If we found any addresses to shadow, run the delivery, and stick any + message back into the shadow_message field in the original. */ + + if (shadow_addr) + { + int save_count = transport_count; + + DEBUG(D_deliver|D_transport) + debug_printf(">>>>>>>>>>>>>>>> Shadow delivery >>>>>>>>>>>>>>>>\n"); + deliver_local(shadow_addr, TRUE); + + for(; shadow_addr; shadow_addr = shadow_addr->next) + { + int sresult = shadow_addr->transport_return; + *(uschar **)shadow_addr->shadow_message = + sresult == OK + ? string_sprintf(" ST=%s", stp->name) + : string_sprintf(" ST=%s (%s%s%s)", stp->name, + shadow_addr->basic_errno <= 0 + ? US"" + : US strerror(shadow_addr->basic_errno), + shadow_addr->basic_errno <= 0 || !shadow_addr->message + ? US"" + : US": ", + shadow_addr->message + ? shadow_addr->message + : shadow_addr->basic_errno <= 0 + ? US"unknown error" + : US""); + + DEBUG(D_deliver|D_transport) + debug_printf("%s shadow transport returned %s for %s\n", + stp->name, rc_to_string(sresult), shadow_addr->address); + } + + DEBUG(D_deliver|D_transport) + debug_printf(">>>>>>>>>>>>>>>> End shadow delivery >>>>>>>>>>>>>>>>\n"); + + transport_count = save_count; /* Restore original transport count */ + } + } + + /* Cancel the expansions that were set up for the delivery. */ + + deliver_set_expansions(NULL); + + /* If the transport was parallelism-limited, decrement the hints DB record. */ + + if (serialize_key) enq_end(serialize_key); + + /* Now we can process the results of the real transport. We must take each + address off the chain first, because post_process_one() puts it on another + chain. */ + + for (addr2 = addr; addr2; addr2 = nextaddr) + { + int result = addr2->transport_return; + nextaddr = addr2->next; + + DEBUG(D_deliver|D_transport) + debug_printf("%s transport returned %s for %s\n", + tp->name, rc_to_string(result), addr2->address); + + /* If there is a retry_record, or if delivery is deferred, build a retry + item for setting a new retry time or deleting the old retry record from + the database. These items are handled all together after all addresses + have been handled (so the database is open just for a short time for + updating). */ + + if (result == DEFER || testflag(addr2, af_lt_retry_exists)) + { + int flags = result == DEFER ? 0 : rf_delete; + uschar *retry_key = string_copy(tp->retry_use_local_part + ? addr2->address_retry_key : addr2->domain_retry_key); + *retry_key = 'T'; + retry_add_item(addr2, retry_key, flags); + } + + /* Done with this address */ + + addr2->delivery_time = deliver_time; + post_process_one(addr2, result, logflags, EXIM_DTYPE_TRANSPORT, logchar); + + /* If a pipe delivery generated text to be sent back, the result may be + changed to FAIL, and we must copy this for subsequent addresses in the + batch. */ + + if (addr2->transport_return != result) + { + for (addr3 = nextaddr; addr3; addr3 = addr3->next) + { + addr3->transport_return = addr2->transport_return; + addr3->basic_errno = addr2->basic_errno; + addr3->message = addr2->message; + } + result = addr2->transport_return; + } + + /* Whether or not the result was changed to FAIL, we need to copy the + return_file value from the first address into all the addresses of the + batch, so they are all listed in the error message. */ + + addr2->return_file = addr->return_file; + + /* Change log character for recording successful deliveries. */ + + if (result == OK) logchar = '-'; + } + } /* Loop back for next batch of addresses */ +} + + + + +/************************************************* +* Sort remote deliveries * +*************************************************/ + +/* This function is called if remote_sort_domains is set. It arranges that the +chain of addresses for remote deliveries is ordered according to the strings +specified. Try to make this shuffling reasonably efficient by handling +sequences of addresses rather than just single ones. + +Arguments: None +Returns: Nothing +*/ + +static void +sort_remote_deliveries(void) +{ +int sep = 0; +address_item **aptr = &addr_remote; +const uschar *listptr = remote_sort_domains; +uschar *pattern; +uschar patbuf[256]; + +/*XXX The list is used before expansion. Not sure how that ties up with the docs */ +while ( *aptr + && (pattern = string_nextinlist(&listptr, &sep, patbuf, sizeof(patbuf))) + ) + { + address_item *moved = NULL; + address_item **bptr = &moved; + + while (*aptr) + { + address_item **next; + deliver_domain = (*aptr)->domain; /* set $domain */ + if (match_isinlist(deliver_domain, (const uschar **)&pattern, UCHAR_MAX+1, + &domainlist_anchor, NULL, MCL_DOMAIN, TRUE, NULL) == OK) + { + aptr = &(*aptr)->next; + continue; + } + + next = &(*aptr)->next; + while ( *next + && (deliver_domain = (*next)->domain, /* Set $domain */ + match_isinlist(deliver_domain, (const uschar **)&pattern, UCHAR_MAX+1, + &domainlist_anchor, NULL, MCL_DOMAIN, TRUE, NULL)) != OK + ) + next = &(*next)->next; + + /* If the batch of non-matchers is at the end, add on any that were + extracted further up the chain, and end this iteration. Otherwise, + extract them from the chain and hang on the moved chain. */ + + if (!*next) + { + *next = moved; + break; + } + + *bptr = *aptr; + *aptr = *next; + *next = NULL; + bptr = next; + aptr = &(*aptr)->next; + } + + /* If the loop ended because the final address matched, *aptr will + be NULL. Add on to the end any extracted non-matching addresses. If + *aptr is not NULL, the loop ended via "break" when *next is null, that + is, there was a string of non-matching addresses at the end. In this + case the extracted addresses have already been added on the end. */ + + if (!*aptr) *aptr = moved; + } + +DEBUG(D_deliver) + { + debug_printf("remote addresses after sorting:\n"); + for (address_item * addr = addr_remote; addr; addr = addr->next) + debug_printf(" %s\n", addr->address); + } +} + + + +/************************************************* +* Read from pipe for remote delivery subprocess * +*************************************************/ + +/* This function is called when the subprocess is complete, but can also be +called before it is complete, in order to empty a pipe that is full (to prevent +deadlock). It must therefore keep track of its progress in the parlist data +block. + +We read the pipe to get the delivery status codes and a possible error message +for each address, optionally preceded by unusability data for the hosts and +also by optional retry data. + +Read in large chunks into the big buffer and then scan through, interpreting +the data therein. In most cases, only a single read will be necessary. No +individual item will ever be anywhere near 2500 bytes in length, so by ensuring +that we read the next chunk when there is less than 2500 bytes left in the +non-final chunk, we can assume each item is complete in the buffer before +handling it. Each item is written using a single write(), which is atomic for +small items (less than PIPE_BUF, which seems to be at least 512 in any Unix and +often bigger) so even if we are reading while the subprocess is still going, we +should never have only a partial item in the buffer. + +hs12: This assumption is not true anymore, since we get quite large items (certificate +information and such). + +Argument: + poffset the offset of the parlist item + eop TRUE if the process has completed + +Returns: TRUE if the terminating 'Z' item has been read, + or there has been a disaster (i.e. no more data needed); + FALSE otherwise +*/ + +static BOOL +par_read_pipe(int poffset, BOOL eop) +{ +host_item *h; +pardata *p = parlist + poffset; +address_item *addrlist = p->addrlist; +address_item *addr = p->addr; +pid_t pid = p->pid; +int fd = p->fd; + +uschar *msg = p->msg; +BOOL done = p->done; + +/* Loop through all items, reading from the pipe when necessary. The pipe +used to be non-blocking. But I do not see a reason for using non-blocking I/O +here, as the preceding poll() tells us, if data is available for reading. + +A read() on a "selected" handle should never block, but(!) it may return +less data then we expected. (The buffer size we pass to read() shouldn't be +understood as a "request", but as a "limit".) + +Each separate item is written to the pipe in a timely manner. But, especially for +larger items, the read(2) may already return partial data from the write(2). + +The write is atomic mostly (depending on the amount written), but atomic does +not imply "all or noting", it just is "not intermixed" with other writes on the +same channel (pipe). + +*/ + +DEBUG(D_deliver) debug_printf("reading pipe for subprocess %d (%s)\n", + (int)p->pid, eop? "ended" : "not ended yet"); + +while (!done) + { + retry_item *r, **rp; + uschar pipeheader[PIPE_HEADER_SIZE+1]; + uschar *id = &pipeheader[0]; + uschar *subid = &pipeheader[1]; + uschar *ptr = big_buffer; + size_t required = PIPE_HEADER_SIZE; /* first the pipehaeder, later the data */ + ssize_t got; + + DEBUG(D_deliver) debug_printf( + "expect %lu bytes (pipeheader) from tpt process %d\n", (u_long)required, pid); + + /* We require(!) all the PIPE_HEADER_SIZE bytes here, as we know, + they're written in a timely manner, so waiting for the write shouldn't hurt a lot. + If we get less, we can assume the subprocess do be done and do not expect any further + information from it. */ + + if ((got = readn(fd, pipeheader, required)) != required) + { + msg = string_sprintf("got " SSIZE_T_FMT " of %d bytes (pipeheader) " + "from transport process %d for transport %s", + got, PIPE_HEADER_SIZE, pid, addr->transport->driver_name); + done = TRUE; + break; + } + + pipeheader[PIPE_HEADER_SIZE] = '\0'; + DEBUG(D_deliver) + debug_printf("got %ld bytes (pipeheader) from transport process %d\n", + (long) got, pid); + + { + /* If we can't decode the pipeheader, the subprocess seems to have a + problem, we do not expect any furher information from it. */ + char *endc; + required = Ustrtol(pipeheader+2, &endc, 10); + if (*endc) + { + msg = string_sprintf("failed to read pipe " + "from transport process %d for transport %s: error decoding size from header", + pid, addr->transport->driver_name); + done = TRUE; + break; + } + } + + DEBUG(D_deliver) + debug_printf("expect %lu bytes (pipedata) from transport process %d\n", + (u_long)required, pid); + + /* Same as above, the transport process will write the bytes announced + in a timely manner, so we can just wait for the bytes, getting less than expected + is considered a problem of the subprocess, we do not expect anything else from it. */ + if ((got = readn(fd, big_buffer, required)) != required) + { + msg = string_sprintf("got only " SSIZE_T_FMT " of " SIZE_T_FMT + " bytes (pipedata) from transport process %d for transport %s", + got, required, pid, addr->transport->driver_name); + done = TRUE; + break; + } + + /* Handle each possible type of item, assuming the complete item is + available in store. */ + + switch (*id) + { + /* Host items exist only if any hosts were marked unusable. Match + up by checking the IP address. */ + + case 'H': + for (h = addrlist->host_list; h; h = h->next) + { + if (!h->address || Ustrcmp(h->address, ptr+2) != 0) continue; + h->status = ptr[0]; + h->why = ptr[1]; + } + ptr += 2; + while (*ptr++); + break; + + /* Retry items are sent in a preceding R item for each address. This is + kept separate to keep each message short enough to guarantee it won't + be split in the pipe. Hopefully, in the majority of cases, there won't in + fact be any retry items at all. + + The complete set of retry items might include an item to delete a + routing retry if there was a previous routing delay. However, routing + retries are also used when a remote transport identifies an address error. + In that case, there may also be an "add" item for the same key. Arrange + that a "delete" item is dropped in favour of an "add" item. */ + + case 'R': + if (!addr) goto ADDR_MISMATCH; + + DEBUG(D_deliver|D_retry) + debug_printf("reading retry information for %s from subprocess\n", + ptr+1); + + /* Cut out any "delete" items on the list. */ + + for (rp = &addr->retries; (r = *rp); rp = &r->next) + if (Ustrcmp(r->key, ptr+1) == 0) /* Found item with same key */ + { + if (!(r->flags & rf_delete)) break; /* It was not "delete" */ + *rp = r->next; /* Excise a delete item */ + DEBUG(D_deliver|D_retry) + debug_printf(" existing delete item dropped\n"); + } + + /* We want to add a delete item only if there is no non-delete item; + however we still have to step ptr through the data. */ + + if (!r || !(*ptr & rf_delete)) + { + r = store_get(sizeof(retry_item), GET_UNTAINTED); + r->next = addr->retries; + addr->retries = r; + r->flags = *ptr++; + r->key = string_copy(ptr); + while (*ptr++); + memcpy(&r->basic_errno, ptr, sizeof(r->basic_errno)); + ptr += sizeof(r->basic_errno); + memcpy(&r->more_errno, ptr, sizeof(r->more_errno)); + ptr += sizeof(r->more_errno); + r->message = *ptr ? string_copy(ptr) : NULL; + DEBUG(D_deliver|D_retry) debug_printf(" added %s item\n", + r->flags & rf_delete ? "delete" : "retry"); + } + + else + { + DEBUG(D_deliver|D_retry) + debug_printf(" delete item not added: non-delete item exists\n"); + ptr++; + while(*ptr++); + ptr += sizeof(r->basic_errno) + sizeof(r->more_errno); + } + + while(*ptr++); + break; + + /* Put the amount of data written into the parlist block */ + + case 'S': + memcpy(&(p->transport_count), ptr, sizeof(transport_count)); + ptr += sizeof(transport_count); + break; + + /* Address items are in the order of items on the address chain. We + remember the current address value in case this function is called + several times to empty the pipe in stages. Information about delivery + over TLS is sent in a preceding X item for each address. We don't put + it in with the other info, in order to keep each message short enough to + guarantee it won't be split in the pipe. */ + +#ifndef DISABLE_TLS + case 'X': + if (!addr) goto ADDR_MISMATCH; /* Below, in 'A' handler */ + switch (*subid) + { + case '1': + addr->tlsver = addr->cipher = addr->peerdn = NULL; + + if (*ptr) + { + addr->cipher = string_copy(ptr); + addr->tlsver = string_copyn(ptr, Ustrchr(ptr, ':') - ptr); + } + while (*ptr++); + if (*ptr) + addr->peerdn = string_copy(ptr); + break; + + case '2': + if (*ptr) + (void) tls_import_cert(ptr, &addr->peercert); + else + addr->peercert = NULL; + break; + + case '3': + if (*ptr) + (void) tls_import_cert(ptr, &addr->ourcert); + else + addr->ourcert = NULL; + break; + +# ifndef DISABLE_OCSP + case '4': + addr->ocsp = *ptr ? *ptr - '0' : OCSP_NOT_REQ; + break; +# endif + } + while (*ptr++); + break; +#endif /*DISABLE_TLS*/ + + case 'C': /* client authenticator information */ + switch (*subid) + { + case '1': addr->authenticator = *ptr ? string_copy(ptr) : NULL; break; + case '2': addr->auth_id = *ptr ? string_copy(ptr) : NULL; break; + case '3': addr->auth_sndr = *ptr ? string_copy(ptr) : NULL; break; + } + while (*ptr++); + break; + +#ifndef DISABLE_PRDR + case 'P': + setflag(addr, af_prdr_used); + break; +#endif + + case 'L': + switch (*subid) + { +#ifndef DISABLE_PIPE_CONNECT + case 2: setflag(addr, af_early_pipe); /*FALLTHROUGH*/ +#endif + case 1: setflag(addr, af_pipelining); break; + } + break; + + case 'K': + setflag(addr, af_chunking_used); + break; + + case 'T': + setflag(addr, af_tcp_fastopen_conn); + if (*subid > '0') setflag(addr, af_tcp_fastopen); + if (*subid > '1') setflag(addr, af_tcp_fastopen_data); + break; + + case 'D': + if (!addr) goto ADDR_MISMATCH; + memcpy(&(addr->dsn_aware), ptr, sizeof(addr->dsn_aware)); + ptr += sizeof(addr->dsn_aware); + DEBUG(D_deliver) debug_printf("DSN read: addr->dsn_aware = %d\n", addr->dsn_aware); + break; + + case 'A': + if (!addr) + { + ADDR_MISMATCH: + msg = string_sprintf("address count mismatch for data read from pipe " + "for transport process %d for transport %s", pid, + addrlist->transport->driver_name); + done = TRUE; + break; + } + + switch (*subid) + { + case 3: /* explicit notification of continued-connection (non)use; + overrides caller's knowlege. */ + if (*ptr & BIT(1)) setflag(addr, af_new_conn); + else if (*ptr & BIT(2)) setflag(addr, af_cont_conn); + break; + +#ifdef SUPPORT_SOCKS + case '2': /* proxy information; must arrive before A0 and applies to that addr XXX oops*/ + proxy_session = TRUE; /*XXX should this be cleared somewhere? */ + if (*ptr == 0) + ptr++; + else + { + proxy_local_address = string_copy(ptr); + while(*ptr++); + memcpy(&proxy_local_port, ptr, sizeof(proxy_local_port)); + ptr += sizeof(proxy_local_port); + } + break; +#endif + +#ifdef EXPERIMENTAL_DSN_INFO + case '1': /* must arrive before A0, and applies to that addr */ + /* Two strings: smtp_greeting and helo_response */ + addr->smtp_greeting = string_copy(ptr); + while(*ptr++); + addr->helo_response = string_copy(ptr); + while(*ptr++); + break; +#endif + + case '0': + DEBUG(D_deliver) debug_printf("A0 %s tret %d\n", addr->address, *ptr); + addr->transport_return = *ptr++; + addr->special_action = *ptr++; + memcpy(&addr->basic_errno, ptr, sizeof(addr->basic_errno)); + ptr += sizeof(addr->basic_errno); + memcpy(&addr->more_errno, ptr, sizeof(addr->more_errno)); + ptr += sizeof(addr->more_errno); + memcpy(&addr->delivery_time, ptr, sizeof(addr->delivery_time)); + ptr += sizeof(addr->delivery_time); + memcpy(&addr->flags, ptr, sizeof(addr->flags)); + ptr += sizeof(addr->flags); + addr->message = *ptr ? string_copy(ptr) : NULL; + while(*ptr++); + addr->user_message = *ptr ? string_copy(ptr) : NULL; + while(*ptr++); + + /* Always two strings for host information, followed by the port number and DNSSEC mark */ + + if (*ptr) + { + h = store_get(sizeof(host_item), GET_UNTAINTED); + h->name = string_copy(ptr); + while (*ptr++); + h->address = string_copy(ptr); + while(*ptr++); + memcpy(&h->port, ptr, sizeof(h->port)); + ptr += sizeof(h->port); + h->dnssec = *ptr == '2' ? DS_YES + : *ptr == '1' ? DS_NO + : DS_UNK; + ptr++; + addr->host_used = h; + } + else ptr++; + + /* Finished with this address */ + + addr = addr->next; + break; + } + break; + + /* Local interface address/port */ + case 'I': + if (*ptr) sending_ip_address = string_copy(ptr); + while (*ptr++) ; + if (*ptr) sending_port = atoi(CS ptr); + while (*ptr++) ; + break; + + /* Z marks the logical end of the data. It is followed by '0' if + continue_transport was NULL at the end of transporting, otherwise '1'. + We need to know when it becomes NULL during a delivery down a passed SMTP + channel so that we don't try to pass anything more down it. Of course, for + most normal messages it will remain NULL all the time. */ + + case 'Z': + if (*ptr == '0') + { + continue_transport = NULL; + continue_hostname = NULL; + } + done = TRUE; + DEBUG(D_deliver) debug_printf("Z0%c item read\n", *ptr); + break; + + /* Anything else is a disaster. */ + + default: + msg = string_sprintf("malformed data (%d) read from pipe for transport " + "process %d for transport %s", ptr[-1], pid, + addr->transport->driver_name); + done = TRUE; + break; + } + } + +/* The done flag is inspected externally, to determine whether or not to +call the function again when the process finishes. */ + +p->done = done; + +/* If the process hadn't finished, and we haven't seen the end of the data +or if we suffered a disaster, update the rest of the state, and return FALSE to +indicate "not finished". */ + +if (!eop && !done) + { + p->addr = addr; + p->msg = msg; + return FALSE; + } + +/* Close our end of the pipe, to prevent deadlock if the far end is still +pushing stuff into it. */ + +(void)close(fd); +p->fd = -1; + +/* If we have finished without error, but haven't had data for every address, +something is wrong. */ + +if (!msg && addr) + msg = string_sprintf("insufficient address data read from pipe " + "for transport process %d for transport %s", pid, + addr->transport->driver_name); + +/* If an error message is set, something has gone wrong in getting back +the delivery data. Put the message into each address and freeze it. */ + +if (msg) + for (addr = addrlist; addr; addr = addr->next) + { + addr->transport_return = DEFER; + addr->special_action = SPECIAL_FREEZE; + addr->message = msg; + log_write(0, LOG_MAIN|LOG_PANIC, "Delivery status for %s: %s\n", addr->address, addr->message); + } + +/* Return TRUE to indicate we have got all we need from this process, even +if it hasn't actually finished yet. */ + +return TRUE; +} + + + +/************************************************* +* Post-process a set of remote addresses * +*************************************************/ + +/* Do what has to be done immediately after a remote delivery for each set of +addresses, then re-write the spool if necessary. Note that post_process_one +puts the address on an appropriate queue; hence we must fish off the next +one first. This function is also called if there is a problem with setting +up a subprocess to do a remote delivery in parallel. In this case, the final +argument contains a message, and the action must be forced to DEFER. + +Argument: + addr pointer to chain of address items + logflags flags for logging + msg NULL for normal cases; -> error message for unexpected problems + fallback TRUE if processing fallback hosts + +Returns: nothing +*/ + +static void +remote_post_process(address_item *addr, int logflags, uschar *msg, + BOOL fallback) +{ +/* If any host addresses were found to be unusable, add them to the unusable +tree so that subsequent deliveries don't try them. */ + +for (host_item * h = addr->host_list; h; h = h->next) + if (h->address) + if (h->status >= hstatus_unusable) tree_add_unusable(h); + +/* Now handle each address on the chain. The transport has placed '=' or '-' +into the special_action field for each successful delivery. */ + +while (addr) + { + address_item *next = addr->next; + + /* If msg == NULL (normal processing) and the result is DEFER and we are + processing the main hosts and there are fallback hosts available, put the + address on the list for fallback delivery. */ + + if ( addr->transport_return == DEFER + && addr->fallback_hosts + && !fallback + && !msg + ) + { + addr->host_list = addr->fallback_hosts; + addr->next = addr_fallback; + addr_fallback = addr; + DEBUG(D_deliver) debug_printf("%s queued for fallback host(s)\n", addr->address); + } + + /* If msg is set (=> unexpected problem), set it in the address before + doing the ordinary post processing. */ + + else + { + if (msg) + { + addr->message = msg; + addr->transport_return = DEFER; + } + (void)post_process_one(addr, addr->transport_return, logflags, + EXIM_DTYPE_TRANSPORT, addr->special_action); + } + + /* Next address */ + + addr = next; + } + +/* If we have just delivered down a passed SMTP channel, and that was +the last address, the channel will have been closed down. Now that +we have logged that delivery, set continue_sequence to 1 so that +any subsequent deliveries don't get "*" incorrectly logged. */ + +if (!continue_transport) continue_sequence = 1; +} + + + +/************************************************* +* Wait for one remote delivery subprocess * +*************************************************/ + +/* This function is called while doing remote deliveries when either the +maximum number of processes exist and we need one to complete so that another +can be created, or when waiting for the last ones to complete. It must wait for +the completion of one subprocess, empty the control block slot, and return a +pointer to the address chain. + +Arguments: none +Returns: pointer to the chain of addresses handled by the process; + NULL if no subprocess found - this is an unexpected error +*/ + +static address_item * +par_wait(void) +{ +int poffset, status; +address_item * addr, * addrlist; +pid_t pid; + +set_process_info("delivering %s: waiting for a remote delivery subprocess " + "to finish", message_id); + +/* Loop until either a subprocess completes, or there are no subprocesses in +existence - in which case give an error return. We cannot proceed just by +waiting for a completion, because a subprocess may have filled up its pipe, and +be waiting for it to be emptied. Therefore, if no processes have finished, we +wait for one of the pipes to acquire some data by calling poll(), with a +timeout just in case. + +The simple approach is just to iterate after reading data from a ready pipe. +This leads to non-ideal behaviour when the subprocess has written its final Z +item, closed the pipe, and is in the process of exiting (the common case). A +call to waitpid() yields nothing completed, but poll() shows the pipe ready - +reading it yields EOF, so you end up with busy-waiting until the subprocess has +actually finished. + +To avoid this, if all the data that is needed has been read from a subprocess +after poll(), an explicit wait() for it is done. We know that all it is doing +is writing to the pipe and then exiting, so the wait should not be long. + +The non-blocking waitpid() is to some extent just insurance; if we could +reliably detect end-of-file on the pipe, we could always know when to do a +blocking wait() for a completed process. However, because some systems use +NDELAY, which doesn't distinguish between EOF and pipe empty, it is easier to +use code that functions without the need to recognize EOF. + +There's a double loop here just in case we end up with a process that is not in +the list of remote delivery processes. Something has obviously gone wrong if +this is the case. (For example, a process that is incorrectly left over from +routing or local deliveries might be found.) The damage can be minimized by +looping back and looking for another process. If there aren't any, the error +return will happen. */ + +for (;;) /* Normally we do not repeat this loop */ + { + while ((pid = waitpid(-1, &status, WNOHANG)) <= 0) + { + int readycount; + + /* A return value of -1 can mean several things. If errno != ECHILD, it + either means invalid options (which we discount), or that this process was + interrupted by a signal. Just loop to try the waitpid() again. + + If errno == ECHILD, waitpid() is telling us that there are no subprocesses + in existence. This should never happen, and is an unexpected error. + However, there is a nasty complication when running under Linux. If "strace + -f" is being used under Linux to trace this process and its children, + subprocesses are "stolen" from their parents and become the children of the + tracing process. A general wait such as the one we've just obeyed returns + as if there are no children while subprocesses are running. Once a + subprocess completes, it is restored to the parent, and waitpid(-1) finds + it. Thanks to Joachim Wieland for finding all this out and suggesting a + palliative. + + This does not happen using "truss" on Solaris, nor (I think) with other + tracing facilities on other OS. It seems to be specific to Linux. + + What we do to get round this is to use kill() to see if any of our + subprocesses are still in existence. If kill() gives an OK return, we know + it must be for one of our processes - it can't be for a re-use of the pid, + because if our process had finished, waitpid() would have found it. If any + of our subprocesses are in existence, we proceed to use poll() as if + waitpid() had returned zero. I think this is safe. */ + + if (pid < 0) + { + if (errno != ECHILD) continue; /* Repeats the waitpid() */ + + DEBUG(D_deliver) + debug_printf("waitpid() returned -1/ECHILD: checking explicitly " + "for process existence\n"); + + for (poffset = 0; poffset < remote_max_parallel; poffset++) + { + if ((pid = parlist[poffset].pid) != 0 && kill(pid, 0) == 0) + { + DEBUG(D_deliver) debug_printf("process %d still exists: assume " + "stolen by strace\n", (int)pid); + break; /* With poffset set */ + } + } + + if (poffset >= remote_max_parallel) + { + DEBUG(D_deliver) debug_printf("*** no delivery children found\n"); + return NULL; /* This is the error return */ + } + } + + /* A pid value greater than 0 breaks the "while" loop. A negative value has + been handled above. A return value of zero means that there is at least one + subprocess, but there are no completed subprocesses. See if any pipes are + ready with any data for reading. */ + + DEBUG(D_deliver) debug_printf("polling subprocess pipes\n"); + + for (poffset = 0; poffset < remote_max_parallel; poffset++) + if (parlist[poffset].pid != 0) + { + parpoll[poffset].fd = parlist[poffset].fd; + parpoll[poffset].events = POLLIN; + } + else + parpoll[poffset].fd = -1; + + /* Stick in a 60-second timeout, just in case. */ + + readycount = poll(parpoll, remote_max_parallel, 60 * 1000); + + /* Scan through the pipes and read any that are ready; use the count + returned by poll() to stop when there are no more. Select() can return + with no processes (e.g. if interrupted). This shouldn't matter. + + If par_read_pipe() returns TRUE, it means that either the terminating Z was + read, or there was a disaster. In either case, we are finished with this + process. Do an explicit wait() for the process and break the main loop if + it succeeds. + + It turns out that we have to deal with the case of an interrupted system + call, which can happen on some operating systems if the signal handling is + set up to do that by default. */ + + for (poffset = 0; + readycount > 0 && poffset < remote_max_parallel; + poffset++) + { + if ( (pid = parlist[poffset].pid) != 0 + && parpoll[poffset].revents + ) + { + readycount--; + if (par_read_pipe(poffset, FALSE)) /* Finished with this pipe */ + for (;;) /* Loop for signals */ + { + pid_t endedpid = waitpid(pid, &status, 0); + if (endedpid == pid) goto PROCESS_DONE; + if (endedpid != (pid_t)(-1) || errno != EINTR) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "Unexpected error return " + "%d (errno = %d) from waitpid() for process %d", + (int)endedpid, errno, (int)pid); + } + } + } + + /* Now go back and look for a completed subprocess again. */ + } + + /* A completed process was detected by the non-blocking waitpid(). Find the + data block that corresponds to this subprocess. */ + + for (poffset = 0; poffset < remote_max_parallel; poffset++) + if (pid == parlist[poffset].pid) break; + + /* Found the data block; this is a known remote delivery process. We don't + need to repeat the outer loop. This should be what normally happens. */ + + if (poffset < remote_max_parallel) break; + + /* This situation is an error, but it's probably better to carry on looking + for another process than to give up (as we used to do). */ + + log_write(0, LOG_MAIN|LOG_PANIC, "Process %d finished: not found in remote " + "transport process list", pid); + } /* End of the "for" loop */ + +/* Come here when all the data was completely read after a poll(), and +the process in pid has been wait()ed for. */ + +PROCESS_DONE: + +DEBUG(D_deliver) + { + if (status == 0) + debug_printf("remote delivery process %d ended\n", (int)pid); + else + debug_printf("remote delivery process %d ended: status=%04x\n", (int)pid, + status); + } + +set_process_info("delivering %s", message_id); + +/* Get the chain of processed addresses */ + +addrlist = parlist[poffset].addrlist; + +/* If the process did not finish cleanly, record an error and freeze (except +for SIGTERM, SIGKILL and SIGQUIT), and also ensure the journal is not removed, +in case the delivery did actually happen. */ + +if ((status & 0xffff) != 0) + { + uschar *msg; + int msb = (status >> 8) & 255; + int lsb = status & 255; + int code = (msb == 0)? (lsb & 0x7f) : msb; + + msg = string_sprintf("%s transport process returned non-zero status 0x%04x: " + "%s %d", + addrlist->transport->driver_name, + status, + msb == 0 ? "terminated by signal" : "exit code", + code); + + if (msb != 0 || (code != SIGTERM && code != SIGKILL && code != SIGQUIT)) + addrlist->special_action = SPECIAL_FREEZE; + + for (addr = addrlist; addr; addr = addr->next) + { + addr->transport_return = DEFER; + addr->message = msg; + } + + remove_journal = FALSE; + } + +/* Else complete reading the pipe to get the result of the delivery, if all +the data has not yet been obtained. */ + +else if (!parlist[poffset].done) + (void) par_read_pipe(poffset, TRUE); + +/* Put the data count and return path into globals, mark the data slot unused, +decrement the count of subprocesses, and return the address chain. */ + +transport_count = parlist[poffset].transport_count; +used_return_path = parlist[poffset].return_path; +parlist[poffset].pid = 0; +parcount--; +return addrlist; +} + + + +/************************************************* +* Wait for subprocesses and post-process * +*************************************************/ + +/* This function waits for subprocesses until the number that are still running +is below a given threshold. For each complete subprocess, the addresses are +post-processed. If we can't find a running process, there is some shambles. +Better not bomb out, as that might lead to multiple copies of the message. Just +log and proceed as if all done. + +Arguments: + max maximum number of subprocesses to leave running + fallback TRUE if processing fallback hosts + +Returns: nothing +*/ + +static void +par_reduce(int max, BOOL fallback) +{ +while (parcount > max) + { + address_item *doneaddr = par_wait(); + if (!doneaddr) + { + log_write(0, LOG_MAIN|LOG_PANIC, + "remote delivery process count got out of step"); + parcount = 0; + } + else + { + transport_instance * tp = doneaddr->transport; + if (tp->max_parallel) + enq_end(string_sprintf("tpt-serialize-%s", tp->name)); + + remote_post_process(doneaddr, LOG_MAIN, NULL, fallback); + } + } +} + +static void +rmt_dlv_checked_write(int fd, char id, char subid, void * buf, ssize_t size) +{ +uschar pipe_header[PIPE_HEADER_SIZE+1]; +size_t total_len = PIPE_HEADER_SIZE + size; + +struct iovec iov[2] = { + { pipe_header, PIPE_HEADER_SIZE }, /* indication about the data to expect */ + { buf, size } /* *the* data */ +}; + +ssize_t ret; + +/* we assume that size can't get larger then BIG_BUFFER_SIZE which currently is set to 16k */ +/* complain to log if someone tries with buffer sizes we can't handle*/ + +if (size > BIG_BUFFER_SIZE-1) + { + log_write(0, LOG_MAIN|LOG_PANIC_DIE, + "Failed writing transport result to pipe: can't handle buffers > %d bytes. truncating!\n", + BIG_BUFFER_SIZE-1); + size = BIG_BUFFER_SIZE; + } + +/* Should we check that we do not write more than PIPE_BUF? What would +that help? */ + +/* convert size to human readable string prepended by id and subid */ +if (PIPE_HEADER_SIZE != snprintf(CS pipe_header, PIPE_HEADER_SIZE+1, "%c%c%05ld", + id, subid, (long)size)) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "header snprintf failed\n"); + +DEBUG(D_deliver) debug_printf("header write id:%c,subid:%c,size:%ld,final:%s\n", + id, subid, (long)size, pipe_header); + +if ((ret = writev(fd, iov, 2)) != total_len) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, + "Failed writing transport result to pipe (%ld of %ld bytes): %s", + (long)ret, (long)total_len, ret == -1 ? strerror(errno) : "short write"); +} + +/************************************************* +* Do remote deliveries * +*************************************************/ + +/* This function is called to process the addresses in addr_remote. We must +pick off the queue all addresses that have the same transport, remote +destination, and errors address, and hand them to the transport in one go, +subject to some configured limitations. If this is a run to continue delivering +to an existing delivery channel, skip all but those addresses that can go to +that channel. The skipped addresses just get deferred. + +If mua_wrapper is set, all addresses must be able to be sent in a single +transaction. If not, this function yields FALSE. + +In Exim 4, remote deliveries are always done in separate processes, even +if remote_max_parallel = 1 or if there's only one delivery to do. The reason +is so that the base process can retain privilege. This makes the +implementation of fallback transports feasible (though not initially done.) + +We create up to the configured number of subprocesses, each of which passes +back the delivery state via a pipe. (However, when sending down an existing +connection, remote_max_parallel is forced to 1.) + +Arguments: + fallback TRUE if processing fallback hosts + +Returns: TRUE normally + FALSE if mua_wrapper is set and the addresses cannot all be sent + in one transaction +*/ + +static BOOL +do_remote_deliveries(BOOL fallback) +{ +int parmax; +int poffset; + +parcount = 0; /* Number of executing subprocesses */ + +/* When sending down an existing channel, only do one delivery at a time. +We use a local variable (parmax) to hold the maximum number of processes; +this gets reduced from remote_max_parallel if we can't create enough pipes. */ + +if (continue_transport) remote_max_parallel = 1; +parmax = remote_max_parallel; + +/* If the data for keeping a list of processes hasn't yet been +set up, do so. */ + +if (!parlist) + { + parlist = store_get(remote_max_parallel * sizeof(pardata), GET_UNTAINTED); + for (poffset = 0; poffset < remote_max_parallel; poffset++) + parlist[poffset].pid = 0; + parpoll = store_get(remote_max_parallel * sizeof(struct pollfd), GET_UNTAINTED); + } + +/* Now loop for each remote delivery */ + +for (int delivery_count = 0; addr_remote; delivery_count++) + { + pid_t pid; + uid_t uid; + gid_t gid; + int pfd[2]; + int address_count = 1; + int address_count_max; + BOOL multi_domain; + BOOL use_initgroups; + BOOL pipe_done = FALSE; + transport_instance *tp; + address_item **anchor = &addr_remote; + address_item *addr = addr_remote; + address_item *last = addr; + address_item *next; + uschar * panicmsg; + uschar * serialize_key = NULL; + + /* Pull the first address right off the list. */ + + addr_remote = addr->next; + addr->next = NULL; + + DEBUG(D_deliver|D_transport) + debug_printf("--------> %s <--------\n", addr->address); + + /* If no transport has been set, there has been a big screw-up somewhere. */ + + if (!(tp = addr->transport)) + { + f.disable_logging = FALSE; /* Jic */ + panicmsg = US"No transport set by router"; + goto panic_continue; + } + + /* Check that this base address hasn't previously been delivered to this + transport. The check is necessary at this point to handle homonymic addresses + correctly in cases where the pattern of redirection changes between delivery + attempts. Non-homonymic previous delivery is detected earlier, at routing + time. */ + + if (previously_transported(addr, FALSE)) continue; + + /* Force failure if the message is too big. */ + + if (tp->message_size_limit) + { + int rc = check_message_size(tp, addr); + if (rc != OK) + { + addr->transport_return = rc; + remote_post_process(addr, LOG_MAIN, NULL, fallback); + continue; + } + } + +/*XXX need to defeat this when DANE is used - but we don't know that yet. +So look out for the place it gets used. +*/ + + /* Get the flag which specifies whether the transport can handle different + domains that nevertheless resolve to the same set of hosts. If it needs + expanding, get variables set: $address_data, $domain_data, $localpart_data, + $host, $host_address, $host_port. */ + if (tp->expand_multi_domain) + deliver_set_expansions(addr); + + if (exp_bool(addr, US"transport", tp->name, D_transport, + US"multi_domain", tp->multi_domain, tp->expand_multi_domain, + &multi_domain) != OK) + { + deliver_set_expansions(NULL); + panicmsg = addr->message; + goto panic_continue; + } + + /* Get the maximum it can handle in one envelope, with zero meaning + unlimited, which is forced for the MUA wrapper case. */ + + address_count_max = tp->max_addresses; + if (address_count_max == 0 || mua_wrapper) address_count_max = 999999; + + + /************************************************************************/ + /***** This is slightly experimental code, but should be safe. *****/ + + /* The address_count_max value is the maximum number of addresses that the + transport can send in one envelope. However, the transport must be capable of + dealing with any number of addresses. If the number it gets exceeds its + envelope limitation, it must send multiple copies of the message. This can be + done over a single connection for SMTP, so uses less resources than making + multiple connections. On the other hand, if remote_max_parallel is greater + than one, it is perhaps a good idea to use parallel processing to move the + message faster, even if that results in multiple simultaneous connections to + the same host. + + How can we come to some compromise between these two ideals? What we do is to + limit the number of addresses passed to a single instance of a transport to + the greater of (a) its address limit (rcpt_max for SMTP) and (b) the total + number of addresses routed to remote transports divided by + remote_max_parallel. For example, if the message has 100 remote recipients, + remote max parallel is 2, and rcpt_max is 10, we'd never send more than 50 at + once. But if rcpt_max is 100, we could send up to 100. + + Of course, not all the remotely addresses in a message are going to go to the + same set of hosts (except in smarthost configurations), so this is just a + heuristic way of dividing up the work. + + Furthermore (1), because this may not be wanted in some cases, and also to + cope with really pathological cases, there is also a limit to the number of + messages that are sent over one connection. This is the same limit that is + used when sending several different messages over the same connection. + Continue_sequence is set when in this situation, to the number sent so + far, including this message. + + Furthermore (2), when somebody explicitly sets the maximum value to 1, it + is probably because they are using VERP, in which case they want to pass only + one address at a time to the transport, in order to be able to use + $local_part and $domain in constructing a new return path. We could test for + the use of these variables, but as it is so likely they will be used when the + maximum is 1, we don't bother. Just leave the value alone. */ + + if ( address_count_max != 1 + && address_count_max < remote_delivery_count/remote_max_parallel + ) + { + int new_max = remote_delivery_count/remote_max_parallel; + int message_max = tp->connection_max_messages; + if (connection_max_messages >= 0) message_max = connection_max_messages; + message_max -= continue_sequence - 1; + if (message_max > 0 && new_max > address_count_max * message_max) + new_max = address_count_max * message_max; + address_count_max = new_max; + } + + /************************************************************************/ + + +/*XXX don't know yet if DANE will be used. So tpt will have to +check at the point if gets next addr from list, and skip/defer any +nonmatch domains +*/ + + /* Pick off all addresses which have the same transport, errors address, + destination, and extra headers. In some cases they point to the same host + list, but we also need to check for identical host lists generated from + entirely different domains. The host list pointers can be NULL in the case + where the hosts are defined in the transport. There is also a configured + maximum limit of addresses that can be handled at once (see comments above + for how it is computed). + If the transport does not handle multiple domains, enforce that also, + and if it might need a per-address check for this, re-evaluate it. + */ + + while ((next = *anchor) && address_count < address_count_max) + { + BOOL md; + if ( (multi_domain || Ustrcmp(next->domain, addr->domain) == 0) + && tp == next->transport + && same_hosts(next->host_list, addr->host_list) + && same_strings(next->prop.errors_address, addr->prop.errors_address) + && same_headers(next->prop.extra_headers, addr->prop.extra_headers) + && same_ugid(tp, next, addr) + && ( next->prop.remove_headers == addr->prop.remove_headers + || ( next->prop.remove_headers + && addr->prop.remove_headers + && Ustrcmp(next->prop.remove_headers, addr->prop.remove_headers) == 0 + ) ) + && ( !multi_domain + || ( ( + (void)(!tp->expand_multi_domain || ((void)deliver_set_expansions(next), 1)), + exp_bool(addr, + US"transport", next->transport->name, D_transport, + US"multi_domain", next->transport->multi_domain, + next->transport->expand_multi_domain, &md) == OK + ) + && md + ) ) ) + { + *anchor = next->next; + next->next = NULL; + next->first = addr; /* remember top one (for retry processing) */ + last->next = next; + last = next; + address_count++; + } + else anchor = &(next->next); + deliver_set_expansions(NULL); + } + + /* If we are acting as an MUA wrapper, all addresses must go in a single + transaction. If not, put them back on the chain and yield FALSE. */ + + if (mua_wrapper && addr_remote) + { + last->next = addr_remote; + addr_remote = addr; + return FALSE; + } + + /* If the transport is limited for parallellism, enforce that here. + The hints DB entry is decremented in par_reduce(), when we reap the + transport process. */ + + if (tpt_parallel_check(tp, addr, &serialize_key)) + if ((panicmsg = expand_string_message)) + goto panic_continue; + else + continue; /* Loop for the next set of addresses. */ + + /* Set up the expansion variables for this set of addresses */ + + deliver_set_expansions(addr); + + /* Ensure any transport-set auth info is fresh */ + addr->authenticator = addr->auth_id = addr->auth_sndr = NULL; + + /* Compute the return path, expanding a new one if required. The old one + must be set first, as it might be referred to in the expansion. */ + + if(addr->prop.errors_address) + return_path = addr->prop.errors_address; + else + return_path = sender_address; + + if (tp->return_path) + { + uschar *new_return_path = expand_string(tp->return_path); + if (new_return_path) + return_path = new_return_path; + else if (!f.expand_string_forcedfail) + { + panicmsg = string_sprintf("Failed to expand return path \"%s\": %s", + tp->return_path, expand_string_message); + goto enq_continue; + } + } + + /* Find the uid, gid, and use_initgroups setting for this transport. Failure + logs and sets up error messages, so we just post-process and continue with + the next address. */ + + if (!findugid(addr, tp, &uid, &gid, &use_initgroups)) + { + panicmsg = NULL; + goto enq_continue; + } + + /* If this transport has a setup function, call it now so that it gets + run in this process and not in any subprocess. That way, the results of + any setup that are retained by the transport can be reusable. One of the + things the setup does is to set the fallback host lists in the addresses. + That is why it is called at this point, before the continue delivery + processing, because that might use the fallback hosts. */ + + if (tp->setup) + (void)((tp->setup)(addr->transport, addr, NULL, uid, gid, NULL)); + + /* If we have a connection still open from a verify stage (lazy-close) + treat it as if it is a continued connection (apart from the counter used + for the log line mark). */ + + if (cutthrough.cctx.sock >= 0 && cutthrough.callout_hold_only) + { + DEBUG(D_deliver) + debug_printf("lazy-callout-close: have conn still open from verification\n"); + continue_transport = cutthrough.transport; + continue_hostname = string_copy(cutthrough.host.name); + continue_host_address = string_copy(cutthrough.host.address); + continue_sequence = 1; + sending_ip_address = cutthrough.snd_ip; + sending_port = cutthrough.snd_port; + smtp_peer_options = cutthrough.peer_options; + } + + /* If this is a run to continue delivery down an already-established + channel, check that this set of addresses matches the transport and + the channel. If it does not, defer the addresses. If a host list exists, + we must check that the continue host is on the list. Otherwise, the + host is set in the transport. */ + + f.continue_more = FALSE; /* In case got set for the last lot */ + if (continue_transport) + { + BOOL ok = Ustrcmp(continue_transport, tp->name) == 0; +/*XXX do we need to check for a DANEd conn vs. a change of domain? */ + + /* If the transport is about to override the host list do not check + it here but take the cost of running the transport process to discover + if the continued_hostname connection is suitable. This is a layering + violation which is unfortunate as it requires we haul in the smtp + include file. */ + + if (ok) + { + smtp_transport_options_block * ob; + + if ( !( Ustrcmp(tp->info->driver_name, "smtp") == 0 + && (ob = (smtp_transport_options_block *)tp->options_block) + && ob->hosts_override && ob->hosts + ) + && addr->host_list + ) + { + ok = FALSE; + for (host_item * h = addr->host_list; h; h = h->next) + if (Ustrcmp(h->name, continue_hostname) == 0) + /*XXX should also check port here */ + { ok = TRUE; break; } + } + } + + /* Addresses not suitable; defer or queue for fallback hosts (which + might be the continue host) and skip to next address. */ + + if (!ok) + { + DEBUG(D_deliver) debug_printf("not suitable for continue_transport (%s)\n", + Ustrcmp(continue_transport, tp->name) != 0 + ? string_sprintf("tpt %s vs %s", continue_transport, tp->name) + : string_sprintf("no host matching %s", continue_hostname)); + if (serialize_key) enq_end(serialize_key); + + if (addr->fallback_hosts && !fallback) + { + for (next = addr; ; next = next->next) + { + next->host_list = next->fallback_hosts; + DEBUG(D_deliver) debug_printf("%s queued for fallback host(s)\n", next->address); + if (!next->next) break; + } + next->next = addr_fallback; + addr_fallback = addr; + } + + else + { + for (next = addr; ; next = next->next) + { + DEBUG(D_deliver) debug_printf(" %s to def list\n", next->address); + if (!next->next) break; + } + next->next = addr_defer; + addr_defer = addr; + } + + continue; + } + + /* Set a flag indicating whether there are further addresses that list + the continued host. This tells the transport to leave the channel open, + but not to pass it to another delivery process. We'd like to do that + for non-continue_transport cases too but the knowlege of which host is + connected to is too hard to manage. Perhaps we need a finer-grain + interface to the transport. */ + + for (next = addr_remote; next && !f.continue_more; next = next->next) + for (host_item * h = next->host_list; h; h = h->next) + if (Ustrcmp(h->name, continue_hostname) == 0) + { f.continue_more = TRUE; break; } + } + + /* The transports set up the process info themselves as they may connect + to more than one remote machine. They also have to set up the filter + arguments, if required, so that the host name and address are available + for expansion. */ + + transport_filter_argv = NULL; + + /* Create the pipe for inter-process communication. If pipe creation + fails, it is probably because the value of remote_max_parallel is so + large that too many file descriptors for pipes have been created. Arrange + to wait for a process to finish, and then try again. If we still can't + create a pipe when all processes have finished, break the retry loop. */ + + while (!pipe_done) + { + if (pipe(pfd) == 0) pipe_done = TRUE; + else if (parcount > 0) parmax = parcount; + else break; + + /* We need to make the reading end of the pipe non-blocking. There are + two different options for this. Exim is cunningly (I hope!) coded so + that it can use either of them, though it prefers O_NONBLOCK, which + distinguishes between EOF and no-more-data. */ + +/* The data appears in a timely manner and we already did a poll on +all pipes, so I do not see a reason to use non-blocking IO here + +#ifdef O_NONBLOCK + (void)fcntl(pfd[pipe_read], F_SETFL, O_NONBLOCK); +#else + (void)fcntl(pfd[pipe_read], F_SETFL, O_NDELAY); +#endif +*/ + + /* If the maximum number of subprocesses already exist, wait for a process + to finish. If we ran out of file descriptors, parmax will have been reduced + from its initial value of remote_max_parallel. */ + + par_reduce(parmax - 1, fallback); + } + + /* If we failed to create a pipe and there were no processes to wait + for, we have to give up on this one. Do this outside the above loop + so that we can continue the main loop. */ + + if (!pipe_done) + { + panicmsg = string_sprintf("unable to create pipe: %s", strerror(errno)); + goto enq_continue; + } + + /* Find a free slot in the pardata list. Must do this after the possible + waiting for processes to finish, because a terminating process will free + up a slot. */ + + for (poffset = 0; poffset < remote_max_parallel; poffset++) + if (parlist[poffset].pid == 0) + break; + + /* If there isn't one, there has been a horrible disaster. */ + + if (poffset >= remote_max_parallel) + { + (void)close(pfd[pipe_write]); + (void)close(pfd[pipe_read]); + panicmsg = US"Unexpectedly no free subprocess slot"; + goto enq_continue; + } + + /* Now fork a subprocess to do the remote delivery, but before doing so, + ensure that any cached resources are released so as not to interfere with + what happens in the subprocess. */ + + search_tidyup(); + + if ((pid = exim_fork(US"transport")) == 0) + { + int fd = pfd[pipe_write]; + host_item *h; + + /* Setting these globals in the subprocess means we need never clear them */ + transport_name = addr->transport->name; + driver_srcfile = tp->srcfile; + driver_srcline = tp->srcline; + + /* There are weird circumstances in which logging is disabled */ + f.disable_logging = tp->disable_logging; + + /* Show pids on debug output if parallelism possible */ + + if (parmax > 1 && (parcount > 0 || addr_remote)) + DEBUG(D_any|D_v) debug_selector |= D_pid; + + /* Reset the random number generator, so different processes don't all + have the same sequence. In the test harness we want different, but + predictable settings for each delivery process, so do something explicit + here rather they rely on the fixed reset in the random number function. */ + + random_seed = f.running_in_test_harness ? 42 + 2*delivery_count : 0; + + /* Set close-on-exec on the pipe so that it doesn't get passed on to + a new process that may be forked to do another delivery down the same + SMTP connection. */ + + (void)fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) | FD_CLOEXEC); + + /* Close open file descriptors for the pipes of other processes + that are running in parallel. */ + + for (poffset = 0; poffset < remote_max_parallel; poffset++) + if (parlist[poffset].pid != 0) (void)close(parlist[poffset].fd); + + /* This process has inherited a copy of the file descriptor + for the data file, but its file pointer is shared with all the + other processes running in parallel. Therefore, we have to re-open + the file in order to get a new file descriptor with its own + file pointer. We don't need to lock it, as the lock is held by + the parent process. There doesn't seem to be any way of doing + a dup-with-new-file-pointer. */ + + (void)close(deliver_datafile); + { + uschar * fname = spool_fname(US"input", message_subdir, message_id, US"-D"); + + if ((deliver_datafile = Uopen(fname, +#ifdef O_CLOEXEC + O_CLOEXEC | +#endif + O_RDWR | O_APPEND, 0)) < 0) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "Failed to reopen %s for remote " + "parallel delivery: %s", fname, strerror(errno)); + } + + /* Set the close-on-exec flag */ +#ifndef O_CLOEXEC + (void)fcntl(deliver_datafile, F_SETFD, fcntl(deliver_datafile, F_GETFD) | + FD_CLOEXEC); +#endif + + /* Set the uid/gid of this process; bombs out on failure. */ + + exim_setugid(uid, gid, use_initgroups, + string_sprintf("remote delivery to %s with transport=%s", + addr->address, tp->name)); + + /* Close the unwanted half of this process' pipe, set the process state, + and run the transport. Afterwards, transport_count will contain the number + of bytes written. */ + + (void)close(pfd[pipe_read]); + set_process_info("delivering %s using %s", message_id, tp->name); + debug_print_string(tp->debug_string); + if (!(tp->info->code)(addr->transport, addr)) replicate_status(addr); + + set_process_info("delivering %s (just run %s for %s%s in subprocess)", + message_id, tp->name, addr->address, addr->next ? ", ..." : ""); + + /* Ensure any cached resources that we used are now released */ + + search_tidyup(); + + /* Pass the result back down the pipe. This is a lot more information + than is needed for a local delivery. We have to send back the error + status for each address, the usability status for each host that is + flagged as unusable, and all the retry items. When TLS is in use, we + send also the cipher and peerdn information. Each type of information + is flagged by an identifying byte, and is then in a fixed format (with + strings terminated by zeros), and there is a final terminator at the + end. The host information and retry information is all attached to + the first address, so that gets sent at the start. */ + + /* Host unusability information: for most success cases this will + be null. */ + + for (h = addr->host_list; h; h = h->next) + { + if (!h->address || h->status < hstatus_unusable) continue; + sprintf(CS big_buffer, "%c%c%s", h->status, h->why, h->address); + rmt_dlv_checked_write(fd, 'H', '0', big_buffer, Ustrlen(big_buffer+2) + 3); + } + + /* The number of bytes written. This is the same for each address. Even + if we sent several copies of the message down the same connection, the + size of each one is the same, and it's that value we have got because + transport_count gets reset before calling transport_write_message(). */ + + memcpy(big_buffer, &transport_count, sizeof(transport_count)); + rmt_dlv_checked_write(fd, 'S', '0', big_buffer, sizeof(transport_count)); + + /* Information about what happened to each address. Four item types are + used: an optional 'X' item first, for TLS information, then an optional "C" + item for any client-auth info followed by 'R' items for any retry settings, + and finally an 'A' item for the remaining data. */ + + for(; addr; addr = addr->next) + { + uschar *ptr; + + /* The certificate verification status goes into the flags */ + if (tls_out.certificate_verified) setflag(addr, af_cert_verified); +#ifdef SUPPORT_DANE + if (tls_out.dane_verified) setflag(addr, af_dane_verified); +#endif +# ifndef DISABLE_TLS_RESUME + if (tls_out.resumption & RESUME_USED) setflag(addr, af_tls_resume); +# endif + + /* Use an X item only if there's something to send */ +#ifndef DISABLE_TLS + if (addr->cipher) + { + ptr = big_buffer + sprintf(CS big_buffer, "%.128s", addr->cipher) + 1; + if (!addr->peerdn) + *ptr++ = 0; + else + ptr += sprintf(CS ptr, "%.512s", addr->peerdn) + 1; + + rmt_dlv_checked_write(fd, 'X', '1', big_buffer, ptr - big_buffer); + } + else if (continue_proxy_cipher) + { + ptr = big_buffer + sprintf(CS big_buffer, "%.128s", continue_proxy_cipher) + 1; + *ptr++ = 0; + rmt_dlv_checked_write(fd, 'X', '1', big_buffer, ptr - big_buffer); + } + + if (addr->peercert) + { + ptr = big_buffer; + if (tls_export_cert(ptr, big_buffer_size-2, addr->peercert)) + while(*ptr++); + else + *ptr++ = 0; + rmt_dlv_checked_write(fd, 'X', '2', big_buffer, ptr - big_buffer); + } + if (addr->ourcert) + { + ptr = big_buffer; + if (tls_export_cert(ptr, big_buffer_size-2, addr->ourcert)) + while(*ptr++); + else + *ptr++ = 0; + rmt_dlv_checked_write(fd, 'X', '3', big_buffer, ptr - big_buffer); + } +# ifndef DISABLE_OCSP + if (addr->ocsp > OCSP_NOT_REQ) + { + ptr = big_buffer + sprintf(CS big_buffer, "%c", addr->ocsp + '0') + 1; + rmt_dlv_checked_write(fd, 'X', '4', big_buffer, ptr - big_buffer); + } +# endif +#endif /*DISABLE_TLS*/ + + if (client_authenticator) + { + ptr = big_buffer + sprintf(CS big_buffer, "%.64s", client_authenticator) + 1; + rmt_dlv_checked_write(fd, 'C', '1', big_buffer, ptr - big_buffer); + } + if (client_authenticated_id) + { + ptr = big_buffer + sprintf(CS big_buffer, "%.64s", client_authenticated_id) + 1; + rmt_dlv_checked_write(fd, 'C', '2', big_buffer, ptr - big_buffer); + } + if (client_authenticated_sender) + { + ptr = big_buffer + sprintf(CS big_buffer, "%.64s", client_authenticated_sender) + 1; + rmt_dlv_checked_write(fd, 'C', '3', big_buffer, ptr - big_buffer); + } + +#ifndef DISABLE_PRDR + if (testflag(addr, af_prdr_used)) + rmt_dlv_checked_write(fd, 'P', '0', NULL, 0); +#endif + + if (testflag(addr, af_pipelining)) +#ifndef DISABLE_PIPE_CONNECT + if (testflag(addr, af_early_pipe)) + rmt_dlv_checked_write(fd, 'L', '2', NULL, 0); + else +#endif + rmt_dlv_checked_write(fd, 'L', '1', NULL, 0); + + if (testflag(addr, af_chunking_used)) + rmt_dlv_checked_write(fd, 'K', '0', NULL, 0); + + if (testflag(addr, af_tcp_fastopen_conn)) + rmt_dlv_checked_write(fd, 'T', + testflag(addr, af_tcp_fastopen) ? testflag(addr, af_tcp_fastopen_data) + ? '2' : '1' : '0', + NULL, 0); + + memcpy(big_buffer, &addr->dsn_aware, sizeof(addr->dsn_aware)); + rmt_dlv_checked_write(fd, 'D', '0', big_buffer, sizeof(addr->dsn_aware)); + + /* Retry information: for most success cases this will be null. */ + + for (retry_item * r = addr->retries; r; r = r->next) + { + sprintf(CS big_buffer, "%c%.500s", r->flags, r->key); + ptr = big_buffer + Ustrlen(big_buffer+2) + 3; + memcpy(ptr, &r->basic_errno, sizeof(r->basic_errno)); + ptr += sizeof(r->basic_errno); + memcpy(ptr, &r->more_errno, sizeof(r->more_errno)); + ptr += sizeof(r->more_errno); + if (!r->message) *ptr++ = 0; else + { + sprintf(CS ptr, "%.512s", r->message); + while(*ptr++); + } + rmt_dlv_checked_write(fd, 'R', '0', big_buffer, ptr - big_buffer); + } + + if (testflag(addr, af_new_conn) || testflag(addr, af_cont_conn)) + { + DEBUG(D_deliver) debug_printf("%scontinued-connection\n", + testflag(addr, af_new_conn) ? "non-" : ""); + big_buffer[0] = testflag(addr, af_new_conn) ? BIT(1) : BIT(2); + rmt_dlv_checked_write(fd, 'A', '3', big_buffer, 1); + } + +#ifdef SUPPORT_SOCKS + if (LOGGING(proxy) && proxy_session) + { + ptr = big_buffer; + if (proxy_local_address) + { + DEBUG(D_deliver) debug_printf("proxy_local_address '%s'\n", proxy_local_address); + ptr = big_buffer + sprintf(CS ptr, "%.128s", proxy_local_address) + 1; + DEBUG(D_deliver) debug_printf("proxy_local_port %d\n", proxy_local_port); + memcpy(ptr, &proxy_local_port, sizeof(proxy_local_port)); + ptr += sizeof(proxy_local_port); + } + else + *ptr++ = '\0'; + rmt_dlv_checked_write(fd, 'A', '2', big_buffer, ptr - big_buffer); + } +#endif + +#ifdef EXPERIMENTAL_DSN_INFO +/*um, are they really per-addr? Other per-conn stuff is not (auth, tls). But host_used is! */ + if (addr->smtp_greeting) + { + DEBUG(D_deliver) debug_printf("smtp_greeting '%s'\n", addr->smtp_greeting); + ptr = big_buffer + sprintf(CS big_buffer, "%.128s", addr->smtp_greeting) + 1; + if (addr->helo_response) + { + DEBUG(D_deliver) debug_printf("helo_response '%s'\n", addr->helo_response); + ptr += sprintf(CS ptr, "%.128s", addr->helo_response) + 1; + } + else + *ptr++ = '\0'; + rmt_dlv_checked_write(fd, 'A', '1', big_buffer, ptr - big_buffer); + } +#endif + + /* The rest of the information goes in an 'A0' item. */ + + sprintf(CS big_buffer, "%c%c", addr->transport_return, addr->special_action); + ptr = big_buffer + 2; + memcpy(ptr, &addr->basic_errno, sizeof(addr->basic_errno)); + ptr += sizeof(addr->basic_errno); + memcpy(ptr, &addr->more_errno, sizeof(addr->more_errno)); + ptr += sizeof(addr->more_errno); + memcpy(ptr, &addr->delivery_time, sizeof(addr->delivery_time)); + ptr += sizeof(addr->delivery_time); + memcpy(ptr, &addr->flags, sizeof(addr->flags)); + ptr += sizeof(addr->flags); + + if (!addr->message) *ptr++ = 0; else + ptr += sprintf(CS ptr, "%.1024s", addr->message) + 1; + + if (!addr->user_message) *ptr++ = 0; else + ptr += sprintf(CS ptr, "%.1024s", addr->user_message) + 1; + + if (!addr->host_used) *ptr++ = 0; else + { + ptr += sprintf(CS ptr, "%.256s", addr->host_used->name) + 1; + ptr += sprintf(CS ptr, "%.64s", addr->host_used->address) + 1; + memcpy(ptr, &addr->host_used->port, sizeof(addr->host_used->port)); + ptr += sizeof(addr->host_used->port); + + /* DNS lookup status */ + *ptr++ = addr->host_used->dnssec==DS_YES ? '2' + : addr->host_used->dnssec==DS_NO ? '1' : '0'; + + } + rmt_dlv_checked_write(fd, 'A', '0', big_buffer, ptr - big_buffer); + } + + /* Local interface address/port */ +#ifdef EXPERIMENTAL_DSN_INFO + if (sending_ip_address) +#else + if (LOGGING(incoming_interface) && sending_ip_address) +#endif + { + uschar * ptr; + ptr = big_buffer + sprintf(CS big_buffer, "%.128s", sending_ip_address) + 1; + ptr += sprintf(CS ptr, "%d", sending_port) + 1; + rmt_dlv_checked_write(fd, 'I', '0', big_buffer, ptr - big_buffer); + } + + /* Add termination flag, close the pipe, and that's it. The character + after 'Z' indicates whether continue_transport is now NULL or not. + A change from non-NULL to NULL indicates a problem with a continuing + connection. */ + + big_buffer[0] = continue_transport ? '1' : '0'; + rmt_dlv_checked_write(fd, 'Z', '0', big_buffer, 1); + (void)close(fd); + exit(EXIT_SUCCESS); + } + + /* Back in the mainline: close the unwanted half of the pipe. */ + + (void)close(pfd[pipe_write]); + + /* If we have a connection still open from a verify stage (lazy-close) + release its TLS library context (if any) as responsibility was passed to + the delivery child process. */ + + if (cutthrough.cctx.sock >= 0 && cutthrough.callout_hold_only) + { +#ifndef DISABLE_TLS + if (cutthrough.is_tls) + tls_close(cutthrough.cctx.tls_ctx, TLS_NO_SHUTDOWN); +#endif + (void) close(cutthrough.cctx.sock); + release_cutthrough_connection(US"passed to transport proc"); + } + + /* Fork failed; defer with error message */ + + if (pid == -1) + { + (void)close(pfd[pipe_read]); + panicmsg = string_sprintf("fork failed for remote delivery to %s: %s", + addr->domain, strerror(errno)); + goto enq_continue; + } + + /* Fork succeeded; increment the count, and remember relevant data for + when the process finishes. */ + + parcount++; + parlist[poffset].addrlist = parlist[poffset].addr = addr; + parlist[poffset].pid = pid; + parlist[poffset].fd = pfd[pipe_read]; + parlist[poffset].done = FALSE; + parlist[poffset].msg = NULL; + parlist[poffset].return_path = return_path; + + /* If the process we've just started is sending a message down an existing + channel, wait for it now. This ensures that only one such process runs at + once, whatever the value of remote_max parallel. Otherwise, we might try to + send two or more messages simultaneously down the same channel. This could + happen if there are different domains that include the same host in otherwise + different host lists. + + Also, if the transport closes down the channel, this information gets back + (continue_transport gets set to NULL) before we consider any other addresses + in this message. */ + + if (continue_transport) par_reduce(0, fallback); + + /* Otherwise, if we are running in the test harness, wait a bit, to let the + newly created process get going before we create another process. This should + ensure repeatability in the tests. Wait long enough for most cases to complete + the transport. */ + + else testharness_pause_ms(600); + + continue; + +enq_continue: + if (serialize_key) enq_end(serialize_key); +panic_continue: + remote_post_process(addr, LOG_MAIN|LOG_PANIC, panicmsg, fallback); + continue; + } + +/* Reached the end of the list of addresses. Wait for all the subprocesses that +are still running and post-process their addresses. */ + +par_reduce(0, fallback); +return TRUE; +} + + + + +/************************************************* +* Split an address into local part and domain * +*************************************************/ + +/* This function initializes an address for routing by splitting it up into a +local part and a domain. The local part is set up twice - once in its original +casing, and once in lower case, and it is dequoted. We also do the "percent +hack" for configured domains. This may lead to a DEFER result if a lookup +defers. When a percent-hacking takes place, we insert a copy of the original +address as a new parent of this address, as if we have had a redirection. + +Argument: + addr points to an addr_item block containing the address + +Returns: OK + DEFER - could not determine if domain is %-hackable +*/ + +int +deliver_split_address(address_item * addr) +{ +uschar * address = addr->address; +uschar * domain; +uschar * t; +int len; + +if (!(domain = Ustrrchr(address, '@'))) + return DEFER; /* should always have a domain, but just in case... */ + +len = domain - address; +addr->domain = string_copylc(domain+1); /* Domains are always caseless */ + +/* The implication in the RFCs (though I can't say I've seen it spelled out +explicitly) is that quoting should be removed from local parts at the point +where they are locally interpreted. [The new draft "821" is more explicit on +this, Jan 1999.] We know the syntax is valid, so this can be done by simply +removing quoting backslashes and any unquoted doublequotes. */ + +t = addr->cc_local_part = store_get(len+1, address); +while(len-- > 0) + { + int c = *address++; + if (c == '\"') continue; + if (c == '\\') + { + *t++ = *address++; + len--; + } + else *t++ = c; + } +*t = 0; + +/* We do the percent hack only for those domains that are listed in +percent_hack_domains. A loop is required, to copy with multiple %-hacks. */ + +if (percent_hack_domains) + { + int rc; + uschar *new_address = NULL; + uschar *local_part = addr->cc_local_part; + + deliver_domain = addr->domain; /* set $domain */ + + while ( (rc = match_isinlist(deliver_domain, (const uschar **)&percent_hack_domains, 0, + &domainlist_anchor, addr->domain_cache, MCL_DOMAIN, TRUE, NULL)) + == OK + && (t = Ustrrchr(local_part, '%')) != NULL + ) + { + new_address = string_copy(local_part); + new_address[t - local_part] = '@'; + deliver_domain = string_copylc(t+1); + local_part = string_copyn(local_part, t - local_part); + } + + if (rc == DEFER) return DEFER; /* lookup deferred */ + + /* If hackery happened, set up new parent and alter the current address. */ + + if (new_address) + { + address_item * new_parent = store_get(sizeof(address_item), GET_UNTAINTED); + *new_parent = *addr; + addr->parent = new_parent; + new_parent->child_count = 1; + addr->address = new_address; + addr->unique = string_copy(new_address); + addr->domain = deliver_domain; + addr->cc_local_part = local_part; + DEBUG(D_deliver) debug_printf("%%-hack changed address to: %s\n", + addr->address); + } + } + +/* Create the lowercased version of the final local part, and make that the +default one to be used. */ + +addr->local_part = addr->lc_local_part = string_copylc(addr->cc_local_part); +return OK; +} + + + + +/************************************************* +* Get next error message text * +*************************************************/ + +/* If f is not NULL, read the next "paragraph", from a customized error message +text file, terminated by a line containing ****, and expand it. + +Arguments: + f NULL or a file to read from + which string indicating which string (for errors) + +Returns: NULL or an expanded string +*/ + +static uschar * +next_emf(FILE *f, uschar *which) +{ +uschar *yield; +gstring * para; +uschar buffer[256]; + +if (!f) return NULL; + +if (!Ufgets(buffer, sizeof(buffer), f) || Ustrcmp(buffer, "****\n") == 0) + return NULL; + +para = string_get(256); +for (;;) + { + para = string_cat(para, buffer); + if (!Ufgets(buffer, sizeof(buffer), f) || Ustrcmp(buffer, "****\n") == 0) + break; + } +if ((yield = expand_string(string_from_gstring(para)))) + return yield; + +log_write(0, LOG_MAIN|LOG_PANIC, "Failed to expand string from " + "bounce_message_file or warn_message_file (%s): %s", which, + expand_string_message); +return NULL; +} + + + + +/************************************************* +* Close down a passed transport channel * +*************************************************/ + +/* This function is called when a passed transport channel cannot be used. +It attempts to close it down tidily. The yield is always DELIVER_NOT_ATTEMPTED +so that the function call can be the argument of a "return" statement. + +Arguments: None +Returns: DELIVER_NOT_ATTEMPTED +*/ + +static int +continue_closedown(void) +{ +if (continue_transport) + for (transport_instance * t = transports; t; t = t->next) + if (Ustrcmp(t->name, continue_transport) == 0) + { + if (t->info->closedown) (t->info->closedown)(t); + break; + } +return DELIVER_NOT_ATTEMPTED; +} + + + + +/************************************************* +* Print address information * +*************************************************/ + +/* This function is called to output an address, or information about an +address, for bounce or defer messages. If the hide_child flag is set, all we +output is the original ancestor address. + +Arguments: + addr points to the address + f the FILE to print to + si an initial string + sc a continuation string for before "generated" + se an end string + +Returns: TRUE if the address is not hidden +*/ + +static BOOL +print_address_information(address_item *addr, FILE *f, uschar *si, uschar *sc, + uschar *se) +{ +BOOL yield = TRUE; +uschar *printed = US""; +address_item *ancestor = addr; +while (ancestor->parent) ancestor = ancestor->parent; + +fprintf(f, "%s", CS si); + +if (addr->parent && testflag(addr, af_hide_child)) + { + printed = US"an undisclosed address"; + yield = FALSE; + } +else if (!testflag(addr, af_pfr) || !addr->parent) + printed = addr->address; + +else + { + uschar *s = addr->address; + uschar *ss; + + if (addr->address[0] == '>') { ss = US"mail"; s++; } + else if (addr->address[0] == '|') ss = US"pipe"; + else ss = US"save"; + + fprintf(f, "%s to %s%sgenerated by ", ss, s, sc); + printed = addr->parent->address; + } + +fprintf(f, "%s", CS string_printing(printed)); + +if (ancestor != addr) + { + uschar *original = ancestor->onetime_parent; + if (!original) original= ancestor->address; + if (strcmpic(original, printed) != 0) + fprintf(f, "%s(%sgenerated from %s)", sc, + ancestor != addr->parent ? "ultimately " : "", + string_printing(original)); + } + +if (addr->host_used) + fprintf(f, "\n host %s [%s]", + addr->host_used->name, addr->host_used->address); + +fprintf(f, "%s", CS se); +return yield; +} + + + + + +/************************************************* +* Print error for an address * +*************************************************/ + +/* This function is called to print the error information out of an address for +a bounce or a warning message. It tries to format the message reasonably by +introducing newlines. All lines are indented by 4; the initial printing +position must be set before calling. + +This function used always to print the error. Nowadays we want to restrict it +to cases such as LMTP/SMTP errors from a remote host, and errors from :fail: +and filter "fail". We no longer pass other information willy-nilly in bounce +and warning messages. Text in user_message is always output; text in message +only if the af_pass_message flag is set. + +Arguments: + addr the address + f the FILE to print on + t some leading text + +Returns: nothing +*/ + +static void +print_address_error(address_item * addr, FILE * f, const uschar * t) +{ +int count = Ustrlen(t); +uschar * s = testflag(addr, af_pass_message) ? addr->message : NULL; + +if (!s && !(s = addr->user_message)) + return; + +fprintf(f, "\n %s", t); + +while (*s) + if (*s == '\\' && s[1] == 'n') + { + fprintf(f, "\n "); + s += 2; + count = 0; + } + else + { + fputc(*s, f); + count++; + if (*s++ == ':' && isspace(*s) && count > 45) + { + fprintf(f, "\n "); /* sic (because space follows) */ + count = 0; + } + } +} + + +/*********************************************************** +* Print Diagnostic-Code for an address * +************************************************************/ + +/* This function is called to print the error information out of an address for +a bounce or a warning message. It tries to format the message reasonably as +required by RFC 3461 by adding a space after each newline + +it uses the same logic as print_address_error() above. if af_pass_message is true +and addr->message is set it uses the remote host answer. if not addr->user_message +is used instead if available. + +Arguments: + addr the address + f the FILE to print on + +Returns: nothing +*/ + +static void +print_dsn_diagnostic_code(const address_item *addr, FILE *f) +{ +uschar * s = testflag(addr, af_pass_message) ? addr->message : NULL; +unsigned cnt; + +/* af_pass_message and addr->message set ? print remote host answer */ +if (s) + { + DEBUG(D_deliver) + debug_printf("DSN Diagnostic-Code: addr->message = %s\n", addr->message); + + /* search first ": ". we assume to find the remote-MTA answer there */ + if (!(s = Ustrstr(addr->message, ": "))) + return; /* not found, bail out */ + s += 2; /* skip ": " */ + cnt = fprintf(f, "Diagnostic-Code: smtp; "); + } +/* no message available. do nothing */ +else return; + +while (*s) + { + if (cnt > 950) /* RFC line length limit: 998 */ + { + DEBUG(D_deliver) debug_printf("print_dsn_diagnostic_code() truncated line\n"); + fputs("[truncated]", f); + break; + } + + if (*s == '\\' && s[1] == 'n') + { + fputs("\n ", f); /* as defined in RFC 3461 */ + s += 2; + cnt += 2; + } + else + { + fputc(*s++, f); + cnt++; + } + } + +fputc('\n', f); +} + + +/************************************************* +* Check list of addresses for duplication * +*************************************************/ + +/* This function was introduced when the test for duplicate addresses that are +not pipes, files, or autoreplies was moved from the middle of routing to when +routing was complete. That was to fix obscure cases when the routing history +affects the subsequent routing of identical addresses. This function is called +after routing, to check that the final routed addresses are not duplicates. + +If we detect a duplicate, we remember what it is a duplicate of. Note that +pipe, file, and autoreply de-duplication is handled during routing, so we must +leave such "addresses" alone here, as otherwise they will incorrectly be +discarded. + +Argument: address of list anchor +Returns: nothing +*/ + +static void +do_duplicate_check(address_item **anchor) +{ +address_item *addr; +while ((addr = *anchor)) + { + tree_node *tnode; + if (testflag(addr, af_pfr)) + { + anchor = &(addr->next); + } + else if ((tnode = tree_search(tree_duplicates, addr->unique))) + { + DEBUG(D_deliver|D_route) + debug_printf("%s is a duplicate address: discarded\n", addr->unique); + *anchor = addr->next; + addr->dupof = tnode->data.ptr; + addr->next = addr_duplicate; + addr_duplicate = addr; + } + else + { + tree_add_duplicate(addr->unique, addr); + anchor = &(addr->next); + } + } +} + + + + +/************************************************/ + +static void +print_dsn_addr_action(FILE * f, address_item * addr, + uschar * action, uschar * status) +{ +address_item * pa; + +if (addr->dsn_orcpt) + fprintf(f,"Original-Recipient: %s\n", addr->dsn_orcpt); + +for (pa = addr; pa->parent; ) pa = pa->parent; +fprintf(f, "Action: %s\n" + "Final-Recipient: rfc822;%s\n" + "Status: %s\n", + action, pa->address, status); +} + + + +/* When running in the test harness, there's an option that allows us to +fudge this time so as to get repeatability of the tests. Take the first +time off the list. In queue runs, the list pointer gets updated in the +calling process. */ + +int +test_harness_fudged_queue_time(int actual_time) +{ +int qt; +if ( f.running_in_test_harness && *fudged_queue_times + && (qt = readconf_readtime(fudged_queue_times, '/', FALSE)) >= 0) + { + DEBUG(D_deliver) debug_printf("fudged queue_times = %s\n", + fudged_queue_times); + return qt; + } +return actual_time; +} + +/************************************************/ + +static FILE * +expand_open(const uschar * filename, + const uschar * varname, const uschar * reason) +{ +const uschar * s = expand_cstring(filename); +FILE * fp = NULL; + +if (!s || !*s) + log_write(0, LOG_MAIN|LOG_PANIC, + "Failed to expand %s: '%s'\n", varname, filename); +else if (*s != '/' || is_tainted(s)) + log_write(0, LOG_MAIN|LOG_PANIC, + "%s is not %s after expansion: '%s'\n", + varname, *s == '/' ? "untainted" : "absolute", s); +else if (!(fp = Ufopen(s, "rb"))) + log_write(0, LOG_MAIN|LOG_PANIC, "Failed to open %s for %s " + "message texts: %s", s, reason, strerror(errno)); +return fp; +} + +/************************************************* +* Deliver one message * +*************************************************/ + +/* This is the function which is called when a message is to be delivered. It +is passed the id of the message. It is possible that the message no longer +exists, if some other process has delivered it, and it is also possible that +the message is being worked on by another process, in which case the data file +will be locked. + +If no delivery is attempted for any of the above reasons, the function returns +DELIVER_NOT_ATTEMPTED. + +If the give_up flag is set true, do not attempt any deliveries, but instead +fail all outstanding addresses and return the message to the sender (or +whoever). + +A delivery operation has a process all to itself; we never deliver more than +one message in the same process. Therefore we needn't worry too much about +store leakage. + +Liable to be called as root. + +Arguments: + id the id of the message to be delivered + forced TRUE if delivery was forced by an administrator; this overrides + retry delays and causes a delivery to be tried regardless + give_up TRUE if an administrator has requested that delivery attempts + be abandoned + +Returns: When the global variable mua_wrapper is FALSE: + DELIVER_ATTEMPTED_NORMAL if a delivery attempt was made + DELIVER_NOT_ATTEMPTED otherwise (see comment above) + When the global variable mua_wrapper is TRUE: + DELIVER_MUA_SUCCEEDED if delivery succeeded + DELIVER_MUA_FAILED if delivery failed + DELIVER_NOT_ATTEMPTED if not attempted (should not occur) +*/ + +int +deliver_message(uschar *id, BOOL forced, BOOL give_up) +{ +int i, rc; +int final_yield = DELIVER_ATTEMPTED_NORMAL; +time_t now = time(NULL); +address_item *addr_last = NULL; +uschar *filter_message = NULL; +int process_recipients = RECIP_ACCEPT; +open_db dbblock; +open_db *dbm_file; +extern int acl_where; +uschar *info; + +#ifdef MEASURE_TIMING +report_time_since(×tamp_startup, US"delivery start"); /* testcase 0022, 2100 */ +#endif + +info = queue_run_pid == (pid_t)0 + ? string_sprintf("delivering %s", id) + : string_sprintf("delivering %s (queue run pid %d)", id, queue_run_pid); + +/* If the D_process_info bit is on, set_process_info() will output debugging +information. If not, we want to show this initial information if D_deliver or +D_queue_run is set or in verbose mode. */ + +set_process_info("%s", info); + +if ( !(debug_selector & D_process_info) + && (debug_selector & (D_deliver|D_queue_run|D_v)) + ) + debug_printf("%s\n", info); + +/* Ensure that we catch any subprocesses that are created. Although Exim +sets SIG_DFL as its initial default, some routes through the code end up +here with it set to SIG_IGN - cases where a non-synchronous delivery process +has been forked, but no re-exec has been done. We use sigaction rather than +plain signal() on those OS where SA_NOCLDWAIT exists, because we want to be +sure it is turned off. (There was a problem on AIX with this.) */ + +#ifdef SA_NOCLDWAIT + { + struct sigaction act; + act.sa_handler = SIG_DFL; + sigemptyset(&(act.sa_mask)); + act.sa_flags = 0; + sigaction(SIGCHLD, &act, NULL); + } +#else +signal(SIGCHLD, SIG_DFL); +#endif + +/* Make the forcing flag available for routers and transports, set up the +global message id field, and initialize the count for returned files and the +message size. This use of strcpy() is OK because the length id is checked when +it is obtained from a command line (the -M or -q options), and otherwise it is +known to be a valid message id. */ + +if (id != message_id) + Ustrcpy(message_id, id); +f.deliver_force = forced; +return_count = 0; +message_size = 0; + +/* Initialize some flags */ + +update_spool = FALSE; +remove_journal = TRUE; + +/* Set a known context for any ACLs we call via expansions */ +acl_where = ACL_WHERE_DELIVERY; + +/* Reset the random number generator, so that if several delivery processes are +started from a queue runner that has already used random numbers (for sorting), +they don't all get the same sequence. */ + +random_seed = 0; + +/* Open and lock the message's data file. Exim locks on this one because the +header file may get replaced as it is re-written during the delivery process. +Any failures cause messages to be written to the log, except for missing files +while queue running - another process probably completed delivery. As part of +opening the data file, message_subdir gets set. */ + +if ((deliver_datafile = spool_open_datafile(id)) < 0) + return continue_closedown(); /* yields DELIVER_NOT_ATTEMPTED */ + +/* The value of message_size at this point has been set to the data length, +plus one for the blank line that notionally precedes the data. */ + +/* Now read the contents of the header file, which will set up the headers in +store, and also the list of recipients and the tree of non-recipients and +assorted flags. It updates message_size. If there is a reading or format error, +give up; if the message has been around for sufficiently long, remove it. */ + + { + uschar * spoolname = string_sprintf("%s-H", id); + if ((rc = spool_read_header(spoolname, TRUE, TRUE)) != spool_read_OK) + { + if (errno == ERRNO_SPOOLFORMAT) + { + struct stat statbuf; + if (Ustat(spool_fname(US"input", message_subdir, spoolname, US""), + &statbuf) == 0) + log_write(0, LOG_MAIN, "Format error in spool file %s: " + "size=" OFF_T_FMT, spoolname, statbuf.st_size); + else + log_write(0, LOG_MAIN, "Format error in spool file %s", spoolname); + } + else + log_write(0, LOG_MAIN, "Error reading spool file %s: %s", spoolname, + strerror(errno)); + + /* If we managed to read the envelope data, received_time contains the + time the message was received. Otherwise, we can calculate it from the + message id. */ + + if (rc != spool_read_hdrerror) + { + received_time.tv_sec = received_time.tv_usec = 0; + /*XXX subsec precision?*/ + for (i = 0; i < 6; i++) + received_time.tv_sec = received_time.tv_sec * BASE_62 + tab62[id[i] - '0']; + } + + /* If we've had this malformed message too long, sling it. */ + + if (now - received_time.tv_sec > keep_malformed) + { + Uunlink(spool_fname(US"msglog", message_subdir, id, US"")); + Uunlink(spool_fname(US"input", message_subdir, id, US"-D")); + Uunlink(spool_fname(US"input", message_subdir, id, US"-H")); + Uunlink(spool_fname(US"input", message_subdir, id, US"-J")); + log_write(0, LOG_MAIN, "Message removed because older than %s", + readconf_printtime(keep_malformed)); + } + + (void)close(deliver_datafile); + deliver_datafile = -1; + return continue_closedown(); /* yields DELIVER_NOT_ATTEMPTED */ + } + } + +/* The spool header file has been read. Look to see if there is an existing +journal file for this message. If there is, it means that a previous delivery +attempt crashed (program or host) before it could update the spool header file. +Read the list of delivered addresses from the journal and add them to the +nonrecipients tree. Then update the spool file. We can leave the journal in +existence, as it will get further successful deliveries added to it in this +run, and it will be deleted if this function gets to its end successfully. +Otherwise it might be needed again. */ + + { + uschar * fname = spool_fname(US"input", message_subdir, id, US"-J"); + FILE * jread; + + if ( (journal_fd = Uopen(fname, O_RDWR|O_APPEND +#ifdef O_CLOEXEC + | O_CLOEXEC +#endif +#ifdef O_NOFOLLOW + | O_NOFOLLOW +#endif + , SPOOL_MODE)) >= 0 + && lseek(journal_fd, 0, SEEK_SET) == 0 + && (jread = fdopen(journal_fd, "rb")) + ) + { + while (Ufgets(big_buffer, big_buffer_size, jread)) + { + int n = Ustrlen(big_buffer); + big_buffer[n-1] = 0; + tree_add_nonrecipient(big_buffer); + DEBUG(D_deliver) debug_printf("Previously delivered address %s taken from " + "journal file\n", big_buffer); + } + rewind(jread); + if ((journal_fd = dup(fileno(jread))) < 0) + journal_fd = fileno(jread); + else + (void) fclose(jread); /* Try to not leak the FILE resource */ + + /* Panic-dies on error */ + (void)spool_write_header(message_id, SW_DELIVERING, NULL); + } + else if (errno != ENOENT) + { + log_write(0, LOG_MAIN|LOG_PANIC, "attempt to open journal for reading gave: " + "%s", strerror(errno)); + return continue_closedown(); /* yields DELIVER_NOT_ATTEMPTED */ + } + + /* A null recipients list indicates some kind of disaster. */ + + if (!recipients_list) + { + (void)close(deliver_datafile); + deliver_datafile = -1; + log_write(0, LOG_MAIN, "Spool error: no recipients for %s", fname); + return continue_closedown(); /* yields DELIVER_NOT_ATTEMPTED */ + } + } + + +/* Handle a message that is frozen. There are a number of different things that +can happen, but in the default situation, unless forced, no delivery is +attempted. */ + +if (f.deliver_freeze) + { +#ifdef SUPPORT_MOVE_FROZEN_MESSAGES + /* Moving to another directory removes the message from Exim's view. Other + tools must be used to deal with it. Logging of this action happens in + spool_move_message() and its subfunctions. */ + + if ( move_frozen_messages + && spool_move_message(id, message_subdir, US"", US"F") + ) + return continue_closedown(); /* yields DELIVER_NOT_ATTEMPTED */ +#endif + + /* For all frozen messages (bounces or not), timeout_frozen_after sets the + maximum time to keep messages that are frozen. Thaw if we reach it, with a + flag causing all recipients to be failed. The time is the age of the + message, not the time since freezing. */ + + if (timeout_frozen_after > 0 && message_age >= timeout_frozen_after) + { + log_write(0, LOG_MAIN, "cancelled by timeout_frozen_after"); + process_recipients = RECIP_FAIL_TIMEOUT; + } + + /* For bounce messages (and others with no sender), thaw if the error message + ignore timer is exceeded. The message will be discarded if this delivery + fails. */ + + else if (!*sender_address && message_age >= ignore_bounce_errors_after) + log_write(0, LOG_MAIN, "Unfrozen by errmsg timer"); + + /* If this is a bounce message, or there's no auto thaw, or we haven't + reached the auto thaw time yet, and this delivery is not forced by an admin + user, do not attempt delivery of this message. Note that forced is set for + continuing messages down the same channel, in order to skip load checking and + ignore hold domains, but we don't want unfreezing in that case. */ + + else + { + if ( ( sender_address[0] == 0 + || auto_thaw <= 0 + || now <= deliver_frozen_at + auto_thaw + ) + && ( !forced || !f.deliver_force_thaw + || !f.admin_user || continue_hostname + ) ) + { + (void)close(deliver_datafile); + deliver_datafile = -1; + log_write(L_skip_delivery, LOG_MAIN, "Message is frozen"); + return continue_closedown(); /* yields DELIVER_NOT_ATTEMPTED */ + } + + /* If delivery was forced (by an admin user), assume a manual thaw. + Otherwise it's an auto thaw. */ + + if (forced) + { + f.deliver_manual_thaw = TRUE; + log_write(0, LOG_MAIN, "Unfrozen by forced delivery"); + } + else log_write(0, LOG_MAIN, "Unfrozen by auto-thaw"); + } + + /* We get here if any of the rules for unfreezing have triggered. */ + + f.deliver_freeze = FALSE; + update_spool = TRUE; + } + + +/* Open the message log file if we are using them. This records details of +deliveries, deferments, and failures for the benefit of the mail administrator. +The log is not used by exim itself to track the progress of a message; that is +done by rewriting the header spool file. */ + +if (message_logs) + { + uschar * fname = spool_fname(US"msglog", message_subdir, id, US""); + uschar * error; + int fd; + + if ((fd = open_msglog_file(fname, SPOOL_MODE, &error)) < 0) + { + log_write(0, LOG_MAIN|LOG_PANIC, "Couldn't %s message log %s: %s", error, + fname, strerror(errno)); + return continue_closedown(); /* yields DELIVER_NOT_ATTEMPTED */ + } + + /* Make a stdio stream out of it. */ + + if (!(message_log = fdopen(fd, "a"))) + { + log_write(0, LOG_MAIN|LOG_PANIC, "Couldn't fdopen message log %s: %s", + fname, strerror(errno)); + return continue_closedown(); /* yields DELIVER_NOT_ATTEMPTED */ + } + } + + +/* If asked to give up on a message, log who did it, and set the action for all +the addresses. */ + +if (give_up) + { + struct passwd *pw = getpwuid(real_uid); + log_write(0, LOG_MAIN, "cancelled by %s", + pw ? US pw->pw_name : string_sprintf("uid %ld", (long int)real_uid)); + process_recipients = RECIP_FAIL; + } + +/* Otherwise, if there are too many Received: headers, fail all recipients. */ + +else if (received_count > received_headers_max) + process_recipients = RECIP_FAIL_LOOP; + +/* Otherwise, if a system-wide, address-independent message filter is +specified, run it now, except in the case when we are failing all recipients as +a result of timeout_frozen_after. If the system filter yields "delivered", then +ignore the true recipients of the message. Failure of the filter file is +logged, and the delivery attempt fails. */ + +else if (system_filter && process_recipients != RECIP_FAIL_TIMEOUT) + { + int rc; + int filtertype; + ugid_block ugid; + redirect_block redirect; + + if (system_filter_uid_set) + { + ugid.uid = system_filter_uid; + ugid.gid = system_filter_gid; + ugid.uid_set = ugid.gid_set = TRUE; + } + else + ugid.uid_set = ugid.gid_set = FALSE; + + return_path = sender_address; + f.enable_dollar_recipients = TRUE; /* Permit $recipients in system filter */ + f.system_filtering = TRUE; + + /* Any error in the filter file causes a delivery to be abandoned. */ + + redirect.string = system_filter; + redirect.isfile = TRUE; + redirect.check_owner = redirect.check_group = FALSE; + redirect.owners = NULL; + redirect.owngroups = NULL; + redirect.pw = NULL; + redirect.modemask = 0; + + DEBUG(D_deliver|D_filter) debug_printf("running system filter\n"); + + rc = rda_interpret( + &redirect, /* Where the data is */ + RDO_DEFER | /* Turn on all the enabling options */ + RDO_FAIL | /* Leave off all the disabling options */ + RDO_FILTER | + RDO_FREEZE | + RDO_REALLOG | + RDO_REWRITE, + NULL, /* No :include: restriction (not used in filter) */ + NULL, /* No sieve vacation directory (not sieve!) */ + NULL, /* No sieve enotify mailto owner (not sieve!) */ + NULL, /* No sieve user address (not sieve!) */ + NULL, /* No sieve subaddress (not sieve!) */ + &ugid, /* uid/gid data */ + &addr_new, /* Where to hang generated addresses */ + &filter_message, /* Where to put error message */ + NULL, /* Don't skip syntax errors */ + &filtertype, /* Will always be set to FILTER_EXIM for this call */ + US"system filter"); /* For error messages */ + + DEBUG(D_deliver|D_filter) debug_printf("system filter returned %d\n", rc); + + if (rc == FF_ERROR || rc == FF_NONEXIST) + { + (void)close(deliver_datafile); + deliver_datafile = -1; + log_write(0, LOG_MAIN|LOG_PANIC, "Error in system filter: %s", + string_printing(filter_message)); + return continue_closedown(); /* yields DELIVER_NOT_ATTEMPTED */ + } + + /* Reset things. If the filter message is an empty string, which can happen + for a filter "fail" or "freeze" command with no text, reset it to NULL. */ + + f.system_filtering = FALSE; + f.enable_dollar_recipients = FALSE; + if (filter_message && filter_message[0] == 0) filter_message = NULL; + + /* Save the values of the system filter variables so that user filters + can use them. */ + + memcpy(filter_sn, filter_n, sizeof(filter_sn)); + + /* The filter can request that delivery of the original addresses be + deferred. */ + + if (rc == FF_DEFER) + { + process_recipients = RECIP_DEFER; + deliver_msglog("Delivery deferred by system filter\n"); + log_write(0, LOG_MAIN, "Delivery deferred by system filter"); + } + + /* The filter can request that a message be frozen, but this does not + take place if the message has been manually thawed. In that case, we must + unset "delivered", which is forced by the "freeze" command to make -bF + work properly. */ + + else if (rc == FF_FREEZE && !f.deliver_manual_thaw) + { + f.deliver_freeze = TRUE; + deliver_frozen_at = time(NULL); + process_recipients = RECIP_DEFER; + frozen_info = string_sprintf(" by the system filter%s%s", + filter_message ? US": " : US"", + filter_message ? filter_message : US""); + } + + /* The filter can request that a message be failed. The error message may be + quite long - it is sent back to the sender in the bounce - but we don't want + to fill up the log with repetitions of it. If it starts with << then the text + between << and >> is written to the log, with the rest left for the bounce + message. */ + + else if (rc == FF_FAIL) + { + uschar *colon = US""; + uschar *logmsg = US""; + int loglen = 0; + + process_recipients = RECIP_FAIL_FILTER; + + if (filter_message) + { + uschar *logend; + colon = US": "; + if ( filter_message[0] == '<' + && filter_message[1] == '<' + && (logend = Ustrstr(filter_message, ">>")) + ) + { + logmsg = filter_message + 2; + loglen = logend - logmsg; + filter_message = logend + 2; + if (filter_message[0] == 0) filter_message = NULL; + } + else + { + logmsg = filter_message; + loglen = Ustrlen(filter_message); + } + } + + log_write(0, LOG_MAIN, "cancelled by system filter%s%.*s", colon, loglen, + logmsg); + } + + /* Delivery can be restricted only to those recipients (if any) that the + filter specified. */ + + else if (rc == FF_DELIVERED) + { + process_recipients = RECIP_IGNORE; + if (addr_new) + log_write(0, LOG_MAIN, "original recipients ignored (system filter)"); + else + log_write(0, LOG_MAIN, "=> discarded (system filter)"); + } + + /* If any new addresses were created by the filter, fake up a "parent" + for them. This is necessary for pipes, etc., which are expected to have + parents, and it also gives some sensible logging for others. Allow + pipes, files, and autoreplies, and run them as the filter uid if set, + otherwise as the current uid. */ + + if (addr_new) + { + int uid = system_filter_uid_set ? system_filter_uid : geteuid(); + int gid = system_filter_gid_set ? system_filter_gid : getegid(); + + /* The text "system-filter" is tested in transport_set_up_command() and in + set_up_shell_command() in the pipe transport, to enable them to permit + $recipients, so don't change it here without also changing it there. */ + + address_item *p = addr_new; + address_item *parent = deliver_make_addr(US"system-filter", FALSE); + + parent->domain = string_copylc(qualify_domain_recipient); + parent->local_part = US"system-filter"; + + /* As part of this loop, we arrange for addr_last to end up pointing + at the final address. This is used if we go on to add addresses for the + original recipients. */ + + while (p) + { + if (parent->child_count == USHRT_MAX) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "system filter generated more " + "than %d delivery addresses", USHRT_MAX); + parent->child_count++; + p->parent = parent; + + if (testflag(p, af_pfr)) + { + uschar *tpname; + uschar *type; + p->uid = uid; + p->gid = gid; + setflag(p, af_uid_set); + setflag(p, af_gid_set); + setflag(p, af_allow_file); + setflag(p, af_allow_pipe); + setflag(p, af_allow_reply); + + /* Find the name of the system filter's appropriate pfr transport */ + + if (p->address[0] == '|') + { + type = US"pipe"; + tpname = system_filter_pipe_transport; + address_pipe = p->address; + } + else if (p->address[0] == '>') + { + type = US"reply"; + tpname = system_filter_reply_transport; + } + else + { + if (p->address[Ustrlen(p->address)-1] == '/') + { + type = US"directory"; + tpname = system_filter_directory_transport; + } + else + { + type = US"file"; + tpname = system_filter_file_transport; + } + address_file = p->address; + } + + /* Now find the actual transport, first expanding the name. We have + set address_file or address_pipe above. */ + + if (tpname) + { + uschar *tmp = expand_string(tpname); + address_file = address_pipe = NULL; + if (!tmp) + p->message = string_sprintf("failed to expand \"%s\" as a " + "system filter transport name", tpname); + if (is_tainted(tmp)) + p->message = string_sprintf("attempt to used tainted value '%s' for" + "transport '%s' as a system filter", tmp, tpname); + tpname = tmp; + } + else + p->message = string_sprintf("system_filter_%s_transport is unset", + type); + + if (tpname) + { + transport_instance *tp; + for (tp = transports; tp; tp = tp->next) + if (Ustrcmp(tp->name, tpname) == 0) + { + p->transport = tp; + break; + } + if (!tp) + p->message = string_sprintf("failed to find \"%s\" transport " + "for system filter delivery", tpname); + } + + /* If we couldn't set up a transport, defer the delivery, putting the + error on the panic log as well as the main log. */ + + if (!p->transport) + { + address_item *badp = p; + p = p->next; + if (!addr_last) addr_new = p; else addr_last->next = p; + badp->local_part = badp->address; /* Needed for log line */ + post_process_one(badp, DEFER, LOG_MAIN|LOG_PANIC, EXIM_DTYPE_ROUTER, 0); + continue; + } + } /* End of pfr handling */ + + /* Either a non-pfr delivery, or we found a transport */ + + DEBUG(D_deliver|D_filter) + debug_printf("system filter added %s\n", p->address); + + addr_last = p; + p = p->next; + } /* Loop through all addr_new addresses */ + } + } + + +/* Scan the recipients list, and for every one that is not in the non- +recipients tree, add an addr item to the chain of new addresses. If the pno +value is non-negative, we must set the onetime parent from it. This which +points to the relevant entry in the recipients list. + +This processing can be altered by the setting of the process_recipients +variable, which is changed if recipients are to be ignored, failed, or +deferred. This can happen as a result of system filter activity, or if the -Mg +option is used to fail all of them. + +Duplicate addresses are handled later by a different tree structure; we can't +just extend the non-recipients tree, because that will be re-written to the +spool if the message is deferred, and in any case there are casing +complications for local addresses. */ + +if (process_recipients != RECIP_IGNORE) + for (i = 0; i < recipients_count; i++) + if (!tree_search(tree_nonrecipients, recipients_list[i].address)) + { + recipient_item *r = recipients_list + i; + address_item *new = deliver_make_addr(r->address, FALSE); + new->prop.errors_address = r->errors_to; +#ifdef SUPPORT_I18N + if ((new->prop.utf8_msg = message_smtputf8)) + { + new->prop.utf8_downcvt = message_utf8_downconvert == 1; + new->prop.utf8_downcvt_maybe = message_utf8_downconvert == -1; + DEBUG(D_deliver) debug_printf("utf8, downconvert %s\n", + new->prop.utf8_downcvt ? "yes" + : new->prop.utf8_downcvt_maybe ? "ifneeded" + : "no"); + } +#endif + + if (r->pno >= 0) + new->onetime_parent = recipients_list[r->pno].address; + + /* If DSN support is enabled, set the dsn flags and the original receipt + to be passed on to other DSN enabled MTAs */ + + new->dsn_flags = r->dsn_flags & rf_dsnflags; + new->dsn_orcpt = r->orcpt; + DEBUG(D_deliver) debug_printf("DSN: set orcpt: %s flags: 0x%x\n", + new->dsn_orcpt ? new->dsn_orcpt : US"", new->dsn_flags); + + switch (process_recipients) + { + /* RECIP_DEFER is set when a system filter freezes a message. */ + + case RECIP_DEFER: + new->next = addr_defer; + addr_defer = new; + break; + + + /* RECIP_FAIL_FILTER is set when a system filter has obeyed a "fail" + command. */ + + case RECIP_FAIL_FILTER: + new->message = + filter_message ? filter_message : US"delivery cancelled"; + setflag(new, af_pass_message); + goto RECIP_QUEUE_FAILED; /* below */ + + + /* RECIP_FAIL_TIMEOUT is set when a message is frozen, but is older + than the value in timeout_frozen_after. Treat non-bounce messages + similarly to -Mg; for bounce messages we just want to discard, so + don't put the address on the failed list. The timeout has already + been logged. */ + + case RECIP_FAIL_TIMEOUT: + new->message = US"delivery cancelled; message timed out"; + goto RECIP_QUEUE_FAILED; /* below */ + + + /* RECIP_FAIL is set when -Mg has been used. */ + + case RECIP_FAIL: + new->message = US"delivery cancelled by administrator"; + /* Fall through */ + + /* Common code for the failure cases above. If this is not a bounce + message, put the address on the failed list so that it is used to + create a bounce. Otherwise do nothing - this just discards the address. + The incident has already been logged. */ + + RECIP_QUEUE_FAILED: + if (sender_address[0]) + { + new->next = addr_failed; + addr_failed = new; + } + break; + + + /* RECIP_FAIL_LOOP is set when there are too many Received: headers + in the message. Process each address as a routing failure; if this + is a bounce message, it will get frozen. */ + + case RECIP_FAIL_LOOP: + new->message = US"Too many \"Received\" headers - suspected mail loop"; + post_process_one(new, FAIL, LOG_MAIN, EXIM_DTYPE_ROUTER, 0); + break; + + + /* Value should be RECIP_ACCEPT; take this as the safe default. */ + + default: + if (!addr_new) addr_new = new; else addr_last->next = new; + addr_last = new; + break; + } + +#ifndef DISABLE_EVENT + if (process_recipients != RECIP_ACCEPT && event_action) + { + uschar * save_local = deliver_localpart; + const uschar * save_domain = deliver_domain; + uschar * addr = new->address, * errmsg = NULL; + int start, end, dom; + + if (!parse_extract_address(addr, &errmsg, &start, &end, &dom, TRUE)) + log_write(0, LOG_MAIN|LOG_PANIC, + "failed to parse address '%.100s': %s\n", addr, errmsg); + else + { + deliver_localpart = + string_copyn(addr+start, dom ? (dom-1) - start : end - start); + deliver_domain = dom ? CUS string_copyn(addr+dom, end - dom) : CUS""; + + (void) event_raise(event_action, US"msg:fail:internal", new->message, NULL); + + deliver_localpart = save_local; + deliver_domain = save_domain; + } + } +#endif + } + +DEBUG(D_deliver) + { + debug_printf("Delivery address list:\n"); + for (address_item * p = addr_new; p; p = p->next) + debug_printf(" %s %s\n", p->address, + p->onetime_parent ? p->onetime_parent : US""); + } + +/* Set up the buffers used for copying over the file when delivering. */ + +deliver_in_buffer = store_malloc(DELIVER_IN_BUFFER_SIZE); +deliver_out_buffer = store_malloc(DELIVER_OUT_BUFFER_SIZE); + + + +/* Until there are no more new addresses, handle each one as follows: + + . If this is a generated address (indicated by the presence of a parent + pointer) then check to see whether it is a pipe, file, or autoreply, and + if so, handle it directly here. The router that produced the address will + have set the allow flags into the address, and also set the uid/gid required. + Having the routers generate new addresses and then checking them here at + the outer level is tidier than making each router do the checking, and + means that routers don't need access to the failed address queue. + + . Break up the address into local part and domain, and make lowercased + versions of these strings. We also make unquoted versions of the local part. + + . Handle the percent hack for those domains for which it is valid. + + . For child addresses, determine if any of the parents have the same address. + If so, generate a different string for previous delivery checking. Without + this code, if the address spqr generates spqr via a forward or alias file, + delivery of the generated spqr stops further attempts at the top level spqr, + which is not what is wanted - it may have generated other addresses. + + . Check on the retry database to see if routing was previously deferred, but + only if in a queue run. Addresses that are to be routed are put on the + addr_route chain. Addresses that are to be deferred are put on the + addr_defer chain. We do all the checking first, so as not to keep the + retry database open any longer than necessary. + + . Now we run the addresses through the routers. A router may put the address + on either the addr_local or the addr_remote chain for local or remote + delivery, respectively, or put it on the addr_failed chain if it is + undeliveable, or it may generate child addresses and put them on the + addr_new chain, or it may defer an address. All the chain anchors are + passed as arguments so that the routers can be called for verification + purposes as well. + + . If new addresses have been generated by the routers, da capo. +*/ + +f.header_rewritten = FALSE; /* No headers rewritten yet */ +while (addr_new) /* Loop until all addresses dealt with */ + { + address_item *addr, *parent; + + /* Failure to open the retry database is treated the same as if it does + not exist. In both cases, dbm_file is NULL. */ + + if (!(dbm_file = dbfn_open(US"retry", O_RDONLY, &dbblock, FALSE, TRUE))) + DEBUG(D_deliver|D_retry|D_route|D_hints_lookup) + debug_printf("no retry data available\n"); + + /* Scan the current batch of new addresses, to handle pipes, files and + autoreplies, and determine which others are ready for routing. */ + + while (addr_new) + { + int rc; + tree_node * tnode; + dbdata_retry * domain_retry_record, * address_retry_record; + + addr = addr_new; + addr_new = addr->next; + + DEBUG(D_deliver|D_retry|D_route) + { + debug_printf(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n"); + debug_printf("Considering: %s\n", addr->address); + } + + /* Handle generated address that is a pipe or a file or an autoreply. */ + + if (testflag(addr, af_pfr)) + { + /* If an autoreply in a filter could not generate a syntactically valid + address, give up forthwith. Set af_ignore_error so that we don't try to + generate a bounce. */ + + if (testflag(addr, af_bad_reply)) + { + addr->basic_errno = ERRNO_BADADDRESS2; + addr->local_part = addr->address; + addr->message = + US"filter autoreply generated syntactically invalid recipient"; + addr->prop.ignore_error = TRUE; + (void) post_process_one(addr, FAIL, LOG_MAIN, EXIM_DTYPE_ROUTER, 0); + continue; /* with the next new address */ + } + + /* If two different users specify delivery to the same pipe or file or + autoreply, there should be two different deliveries, so build a unique + string that incorporates the original address, and use this for + duplicate testing and recording delivery, and also for retrying. */ + + addr->unique = + string_sprintf("%s:%s", addr->address, addr->parent->unique + + (testflag(addr->parent, af_homonym)? 3:0)); + + addr->address_retry_key = addr->domain_retry_key = + string_sprintf("T:%s", addr->unique); + + /* If a filter file specifies two deliveries to the same pipe or file, + we want to de-duplicate, but this is probably not wanted for two mail + commands to the same address, where probably both should be delivered. + So, we have to invent a different unique string in that case. Just + keep piling '>' characters on the front. */ + + if (addr->address[0] == '>') + while (tree_search(tree_duplicates, addr->unique)) + addr->unique = string_sprintf(">%s", addr->unique); + + else if ((tnode = tree_search(tree_duplicates, addr->unique))) + { + DEBUG(D_deliver|D_route) + debug_printf("%s is a duplicate address: discarded\n", addr->address); + addr->dupof = tnode->data.ptr; + addr->next = addr_duplicate; + addr_duplicate = addr; + continue; + } + + DEBUG(D_deliver|D_route) debug_printf("unique = %s\n", addr->unique); + + /* Check for previous delivery */ + + if (tree_search(tree_nonrecipients, addr->unique)) + { + DEBUG(D_deliver|D_route) + debug_printf("%s was previously delivered: discarded\n", addr->address); + child_done(addr, tod_stamp(tod_log)); + continue; + } + + /* Save for checking future duplicates */ + + tree_add_duplicate(addr->unique, addr); + + /* Set local part and domain */ + + addr->local_part = addr->address; + addr->domain = addr->parent->domain; + + /* Ensure that the delivery is permitted. */ + + if (testflag(addr, af_file)) + { + if (!testflag(addr, af_allow_file)) + { + addr->basic_errno = ERRNO_FORBIDFILE; + addr->message = US"delivery to file forbidden"; + (void)post_process_one(addr, FAIL, LOG_MAIN, EXIM_DTYPE_ROUTER, 0); + continue; /* with the next new address */ + } + } + else if (addr->address[0] == '|') + { + if (!testflag(addr, af_allow_pipe)) + { + addr->basic_errno = ERRNO_FORBIDPIPE; + addr->message = US"delivery to pipe forbidden"; + (void)post_process_one(addr, FAIL, LOG_MAIN, EXIM_DTYPE_ROUTER, 0); + continue; /* with the next new address */ + } + } + else if (!testflag(addr, af_allow_reply)) + { + addr->basic_errno = ERRNO_FORBIDREPLY; + addr->message = US"autoreply forbidden"; + (void)post_process_one(addr, FAIL, LOG_MAIN, EXIM_DTYPE_ROUTER, 0); + continue; /* with the next new address */ + } + + /* If the errno field is already set to BADTRANSPORT, it indicates + failure to expand a transport string, or find the associated transport, + or an unset transport when one is required. Leave this test till now so + that the forbid errors are given in preference. */ + + if (addr->basic_errno == ERRNO_BADTRANSPORT) + { + (void)post_process_one(addr, DEFER, LOG_MAIN, EXIM_DTYPE_ROUTER, 0); + continue; + } + + /* Treat /dev/null as a special case and abandon the delivery. This + avoids having to specify a uid on the transport just for this case. + Arrange for the transport name to be logged as "**bypassed**". + Copy the transport for this fairly unusual case rather than having + to make all transports mutable. */ + + if (Ustrcmp(addr->address, "/dev/null") == 0) + { + transport_instance * save_t = addr->transport; + transport_instance * t = store_get(sizeof(*t), save_t); + *t = *save_t; + t->name = US"**bypassed**"; + addr->transport = t; + (void)post_process_one(addr, OK, LOG_MAIN, EXIM_DTYPE_TRANSPORT, '='); + addr->transport= save_t; + continue; /* with the next new address */ + } + + /* Pipe, file, or autoreply delivery is to go ahead as a normal local + delivery. */ + + DEBUG(D_deliver|D_route) + debug_printf("queued for %s transport\n", addr->transport->name); + addr->next = addr_local; + addr_local = addr; + continue; /* with the next new address */ + } + + /* Handle normal addresses. First, split up into local part and domain, + handling the %-hack if necessary. There is the possibility of a defer from + a lookup in percent_hack_domains. */ + + if ((rc = deliver_split_address(addr)) == DEFER) + { + addr->message = US"cannot check percent_hack_domains"; + addr->basic_errno = ERRNO_LISTDEFER; + (void)post_process_one(addr, DEFER, LOG_MAIN, EXIM_DTYPE_NONE, 0); + continue; + } + + /* Check to see if the domain is held. If so, proceed only if the + delivery was forced by hand. */ + + deliver_domain = addr->domain; /* set $domain */ + if ( !forced && hold_domains + && (rc = match_isinlist(addr->domain, (const uschar **)&hold_domains, 0, + &domainlist_anchor, addr->domain_cache, MCL_DOMAIN, TRUE, + NULL)) != FAIL + ) + { + if (rc == DEFER) + { + addr->message = US"hold_domains lookup deferred"; + addr->basic_errno = ERRNO_LISTDEFER; + } + else + { + addr->message = US"domain is held"; + addr->basic_errno = ERRNO_HELD; + } + (void)post_process_one(addr, DEFER, LOG_MAIN, EXIM_DTYPE_NONE, 0); + continue; + } + + /* Now we can check for duplicates and previously delivered addresses. In + order to do this, we have to generate a "unique" value for each address, + because there may be identical actual addresses in a line of descendents. + The "unique" field is initialized to the same value as the "address" field, + but gets changed here to cope with identically-named descendents. */ + + for (parent = addr->parent; parent; parent = parent->parent) + if (strcmpic(addr->address, parent->address) == 0) break; + + /* If there's an ancestor with the same name, set the homonym flag. This + influences how deliveries are recorded. Then add a prefix on the front of + the unique address. We use \n\ where n starts at 0 and increases each time. + It is unlikely to pass 9, but if it does, it may look odd but will still + work. This means that siblings or cousins with the same names are treated + as duplicates, which is what we want. */ + + if (parent) + { + setflag(addr, af_homonym); + if (parent->unique[0] != '\\') + addr->unique = string_sprintf("\\0\\%s", addr->address); + else + addr->unique = string_sprintf("\\%c\\%s", parent->unique[1] + 1, + addr->address); + } + + /* Ensure that the domain in the unique field is lower cased, because + domains are always handled caselessly. */ + + for (uschar * p = Ustrrchr(addr->unique, '@'); *p; p++) *p = tolower(*p); + + DEBUG(D_deliver|D_route) debug_printf("unique = %s\n", addr->unique); + + if (tree_search(tree_nonrecipients, addr->unique)) + { + DEBUG(D_deliver|D_route) + debug_printf("%s was previously delivered: discarded\n", addr->unique); + child_done(addr, tod_stamp(tod_log)); + continue; + } + + /* Get the routing retry status, saving the two retry keys (with and + without the local part) for subsequent use. If there is no retry record for + the standard address routing retry key, we look for the same key with the + sender attached, because this form is used by the smtp transport after a + 4xx response to RCPT when address_retry_include_sender is true. */ + + addr->domain_retry_key = string_sprintf("R:%s", addr->domain); + addr->address_retry_key = string_sprintf("R:%s@%s", addr->local_part, + addr->domain); + + if (dbm_file) + { + domain_retry_record = dbfn_read(dbm_file, addr->domain_retry_key); + if ( domain_retry_record + && now - domain_retry_record->time_stamp > retry_data_expire + ) + { + DEBUG(D_deliver|D_retry) + debug_printf("domain retry record present but expired\n"); + domain_retry_record = NULL; /* Ignore if too old */ + } + + address_retry_record = dbfn_read(dbm_file, addr->address_retry_key); + if ( address_retry_record + && now - address_retry_record->time_stamp > retry_data_expire + ) + { + DEBUG(D_deliver|D_retry) + debug_printf("address retry record present but expired\n"); + address_retry_record = NULL; /* Ignore if too old */ + } + + if (!address_retry_record) + { + uschar *altkey = string_sprintf("%s:<%s>", addr->address_retry_key, + sender_address); + address_retry_record = dbfn_read(dbm_file, altkey); + if ( address_retry_record + && now - address_retry_record->time_stamp > retry_data_expire) + { + DEBUG(D_deliver|D_retry) + debug_printf("address retry record present but expired\n"); + address_retry_record = NULL; /* Ignore if too old */ + } + } + } + else + domain_retry_record = address_retry_record = NULL; + + DEBUG(D_deliver|D_retry) + { + if (!domain_retry_record) + debug_printf("no domain retry record\n"); + else + debug_printf("have domain retry record; next_try = now%+d\n", + f.running_in_test_harness ? 0 : + (int)(domain_retry_record->next_try - now)); + + if (!address_retry_record) + debug_printf("no address retry record\n"); + else + debug_printf("have address retry record; next_try = now%+d\n", + f.running_in_test_harness ? 0 : + (int)(address_retry_record->next_try - now)); + } + + /* If we are sending a message down an existing SMTP connection, we must + assume that the message which created the connection managed to route + an address to that connection. We do not want to run the risk of taking + a long time over routing here, because if we do, the server at the other + end of the connection may time it out. This is especially true for messages + with lots of addresses. For this kind of delivery, queue_running is not + set, so we would normally route all addresses. We take a pragmatic approach + and defer routing any addresses that have any kind of domain retry record. + That is, we don't even look at their retry times. It doesn't matter if this + doesn't work occasionally. This is all just an optimization, after all. + + The reason for not doing the same for address retries is that they normally + arise from 4xx responses, not DNS timeouts. */ + + if (continue_hostname && domain_retry_record) + { + addr->message = US"reusing SMTP connection skips previous routing defer"; + addr->basic_errno = ERRNO_RRETRY; + (void)post_process_one(addr, DEFER, LOG_MAIN, EXIM_DTYPE_ROUTER, 0); + + addr->message = domain_retry_record->text; + setflag(addr, af_pass_message); + } + + /* If we are in a queue run, defer routing unless there is no retry data or + we've passed the next retry time, or this message is forced. In other + words, ignore retry data when not in a queue run. + + However, if the domain retry time has expired, always allow the routing + attempt. If it fails again, the address will be failed. This ensures that + each address is routed at least once, even after long-term routing + failures. + + If there is an address retry, check that too; just wait for the next + retry time. This helps with the case when the temporary error on the + address was really message-specific rather than address specific, since + it allows other messages through. + + We also wait for the next retry time if this is a message sent down an + existing SMTP connection (even though that will be forced). Otherwise there + will be far too many attempts for an address that gets a 4xx error. In + fact, after such an error, we should not get here because, the host should + not be remembered as one this message needs. However, there was a bug that + used to cause this to happen, so it is best to be on the safe side. + + Even if we haven't reached the retry time in the hints, there is one more + check to do, which is for the ultimate address timeout. We only do this + check if there is an address retry record and there is not a domain retry + record; this implies that previous attempts to handle the address had the + retry_use_local_parts option turned on. We use this as an approximation + for the destination being like a local delivery, for example delivery over + LMTP to an IMAP message store. In this situation users are liable to bump + into their quota and thereby have intermittently successful deliveries, + which keep the retry record fresh, which can lead to us perpetually + deferring messages. */ + + else if ( ( f.queue_running && !f.deliver_force + || continue_hostname + ) + && ( ( domain_retry_record + && now < domain_retry_record->next_try + && !domain_retry_record->expired + ) + || ( address_retry_record + && now < address_retry_record->next_try + ) ) + && ( domain_retry_record + || !address_retry_record + || !retry_ultimate_address_timeout(addr->address_retry_key, + addr->domain, address_retry_record, now) + ) ) + { + addr->message = US"retry time not reached"; + addr->basic_errno = ERRNO_RRETRY; + (void)post_process_one(addr, DEFER, LOG_MAIN, EXIM_DTYPE_ROUTER, 0); + + /* For remote-retry errors (here and just above) that we've not yet + hit the retry time, use the error recorded in the retry database + as info in the warning message. This lets us send a message even + when we're not failing on a fresh attempt. We assume that this + info is not sensitive. */ + + addr->message = domain_retry_record + ? domain_retry_record->text : address_retry_record->text; + setflag(addr, af_pass_message); + } + + /* The domain is OK for routing. Remember if retry data exists so it + can be cleaned up after a successful delivery. */ + + else + { + if (domain_retry_record || address_retry_record) + setflag(addr, af_dr_retry_exists); + addr->next = addr_route; + addr_route = addr; + DEBUG(D_deliver|D_route) + debug_printf("%s: queued for routing\n", addr->address); + } + } + + /* The database is closed while routing is actually happening. Requests to + update it are put on a chain and all processed together at the end. */ + + if (dbm_file) dbfn_close(dbm_file); + + /* If queue_domains is set, we don't even want to try routing addresses in + those domains. During queue runs, queue_domains is forced to be unset. + Optimize by skipping this pass through the addresses if nothing is set. */ + + if (!f.deliver_force && queue_domains) + { + address_item *okaddr = NULL; + while (addr_route) + { + address_item *addr = addr_route; + addr_route = addr->next; + + deliver_domain = addr->domain; /* set $domain */ + if ((rc = match_isinlist(addr->domain, CUSS &queue_domains, 0, + &domainlist_anchor, addr->domain_cache, MCL_DOMAIN, TRUE, NULL)) + != OK) + if (rc == DEFER) + { + addr->basic_errno = ERRNO_LISTDEFER; + addr->message = US"queue_domains lookup deferred"; + (void)post_process_one(addr, DEFER, LOG_MAIN, EXIM_DTYPE_ROUTER, 0); + } + else + { + addr->next = okaddr; + okaddr = addr; + } + else + { + addr->basic_errno = ERRNO_QUEUE_DOMAIN; + addr->message = US"domain is in queue_domains"; + (void)post_process_one(addr, DEFER, LOG_MAIN, EXIM_DTYPE_ROUTER, 0); + } + } + + addr_route = okaddr; + } + + /* Now route those addresses that are not deferred. */ + + while (addr_route) + { + int rc; + address_item *addr = addr_route; + const uschar *old_domain = addr->domain; + uschar *old_unique = addr->unique; + addr_route = addr->next; + addr->next = NULL; + + /* Just in case some router parameter refers to it. */ + + if (!(return_path = addr->prop.errors_address)) + return_path = sender_address; + + /* If a router defers an address, add a retry item. Whether or not to + use the local part in the key is a property of the router. */ + + if ((rc = route_address(addr, &addr_local, &addr_remote, &addr_new, + &addr_succeed, v_none)) == DEFER) + retry_add_item(addr, + addr->router->retry_use_local_part + ? string_sprintf("R:%s@%s", addr->local_part, addr->domain) + : string_sprintf("R:%s", addr->domain), + 0); + + /* Otherwise, if there is an existing retry record in the database, add + retry items to delete both forms. We must also allow for the possibility + of a routing retry that includes the sender address. Since the domain might + have been rewritten (expanded to fully qualified) as a result of routing, + ensure that the rewritten form is also deleted. */ + + else if (testflag(addr, af_dr_retry_exists)) + { + uschar *altkey = string_sprintf("%s:<%s>", addr->address_retry_key, + sender_address); + retry_add_item(addr, altkey, rf_delete); + retry_add_item(addr, addr->address_retry_key, rf_delete); + retry_add_item(addr, addr->domain_retry_key, rf_delete); + if (Ustrcmp(addr->domain, old_domain) != 0) + retry_add_item(addr, string_sprintf("R:%s", old_domain), rf_delete); + } + + /* DISCARD is given for :blackhole: and "seen finish". The event has been + logged, but we need to ensure the address (and maybe parents) is marked + done. */ + + if (rc == DISCARD) + { + address_done(addr, tod_stamp(tod_log)); + continue; /* route next address */ + } + + /* The address is finished with (failed or deferred). */ + + if (rc != OK) + { + (void)post_process_one(addr, rc, LOG_MAIN, EXIM_DTYPE_ROUTER, 0); + continue; /* route next address */ + } + + /* The address has been routed. If the router changed the domain, it will + also have changed the unique address. We have to test whether this address + has already been delivered, because it's the unique address that finally + gets recorded. */ + + if ( addr->unique != old_unique + && tree_search(tree_nonrecipients, addr->unique) != 0 + ) + { + DEBUG(D_deliver|D_route) debug_printf("%s was previously delivered: " + "discarded\n", addr->address); + if (addr_remote == addr) addr_remote = addr->next; + else if (addr_local == addr) addr_local = addr->next; + } + + /* If the router has same_domain_copy_routing set, we are permitted to copy + the routing for any other addresses with the same domain. This is an + optimisation to save repeated DNS lookups for "standard" remote domain + routing. The option is settable only on routers that generate host lists. + We play it very safe, and do the optimization only if the address is routed + to a remote transport, there are no header changes, and the domain was not + modified by the router. */ + + if ( addr_remote == addr + && addr->router->same_domain_copy_routing + && !addr->prop.extra_headers + && !addr->prop.remove_headers + && old_domain == addr->domain + ) + { + address_item **chain = &addr_route; + while (*chain) + { + address_item *addr2 = *chain; + if (Ustrcmp(addr2->domain, addr->domain) != 0) + { + chain = &(addr2->next); + continue; + } + + /* Found a suitable address; take it off the routing list and add it to + the remote delivery list. */ + + *chain = addr2->next; + addr2->next = addr_remote; + addr_remote = addr2; + + /* Copy the routing data */ + + addr2->domain = addr->domain; + addr2->router = addr->router; + addr2->transport = addr->transport; + addr2->host_list = addr->host_list; + addr2->fallback_hosts = addr->fallback_hosts; + addr2->prop.errors_address = addr->prop.errors_address; + copyflag(addr2, addr, af_hide_child); + copyflag(addr2, addr, af_local_host_removed); + + DEBUG(D_deliver|D_route) + debug_printf(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n" + "routing %s\n" + "Routing for %s copied from %s\n", + addr2->address, addr2->address, addr->address); + } + } + } /* Continue with routing the next address. */ + } /* Loop to process any child addresses that the routers created, and + any rerouted addresses that got put back on the new chain. */ + + +/* Debugging: show the results of the routing */ + +DEBUG(D_deliver|D_retry|D_route) + { + debug_printf(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n"); + debug_printf("After routing:\n Local deliveries:\n"); + for (address_item * p = addr_local; p; p = p->next) + debug_printf(" %s\n", p->address); + + debug_printf(" Remote deliveries:\n"); + for (address_item * p = addr_remote; p; p = p->next) + debug_printf(" %s\n", p->address); + + debug_printf(" Failed addresses:\n"); + for (address_item * p = addr_failed; p; p = p->next) + debug_printf(" %s\n", p->address); + + debug_printf(" Deferred addresses:\n"); + for (address_item * p = addr_defer; p; p = p->next) + debug_printf(" %s\n", p->address); + } + +/* Free any resources that were cached during routing. */ + +search_tidyup(); +route_tidyup(); + +/* These two variables are set only during routing, after check_local_user. +Ensure they are not set in transports. */ + +local_user_gid = (gid_t)(-1); +local_user_uid = (uid_t)(-1); + +/* Check for any duplicate addresses. This check is delayed until after +routing, because the flexibility of the routing configuration means that +identical addresses with different parentage may end up being redirected to +different addresses. Checking for duplicates too early (as we previously used +to) makes this kind of thing not work. */ + +do_duplicate_check(&addr_local); +do_duplicate_check(&addr_remote); + +/* When acting as an MUA wrapper, we proceed only if all addresses route to a +remote transport. The check that they all end up in one transaction happens in +the do_remote_deliveries() function. */ + +if ( mua_wrapper + && (addr_local || addr_failed || addr_defer) + ) + { + address_item *addr; + uschar *which, *colon, *msg; + + if (addr_local) + { + addr = addr_local; + which = US"local"; + } + else if (addr_defer) + { + addr = addr_defer; + which = US"deferred"; + } + else + { + addr = addr_failed; + which = US"failed"; + } + + while (addr->parent) addr = addr->parent; + + if (addr->message) + { + colon = US": "; + msg = addr->message; + } + else colon = msg = US""; + + /* We don't need to log here for a forced failure as it will already + have been logged. Defer will also have been logged, but as a defer, so we do + need to do the failure logging. */ + + if (addr != addr_failed) + log_write(0, LOG_MAIN, "** %s routing yielded a %s delivery", + addr->address, which); + + /* Always write an error to the caller */ + + fprintf(stderr, "routing %s yielded a %s delivery%s%s\n", addr->address, + which, colon, msg); + + final_yield = DELIVER_MUA_FAILED; + addr_failed = addr_defer = NULL; /* So that we remove the message */ + goto DELIVERY_TIDYUP; + } + + +/* If this is a run to continue deliveries to an external channel that is +already set up, defer any local deliveries. + +jgh 2020/12/20: I don't see why; locals should be quick. +The defer goes back to version 1.62 in 1997. A local being still deliverable +during a continued run might result from something like a defer during the +original delivery, eg. in a DB lookup. Unlikely but possible. + +To avoid delaying a local when combined with a callout-hold for a remote +delivery, test continue_sequence rather than continue_transport. */ + +if (continue_sequence > 1 && addr_local) + { + DEBUG(D_deliver|D_retry|D_route) + debug_printf("deferring local deliveries due to continued-transport\n"); + if (addr_defer) + { + address_item * addr = addr_defer; + while (addr->next) addr = addr->next; + addr->next = addr_local; + } + else + addr_defer = addr_local; + addr_local = NULL; + } + + +/* Because address rewriting can happen in the routers, we should not really do +ANY deliveries until all addresses have been routed, so that all recipients of +the message get the same headers. However, this is in practice not always +possible, since sometimes remote addresses give DNS timeouts for days on end. +The pragmatic approach is to deliver what we can now, saving any rewritten +headers so that at least the next lot of recipients benefit from the rewriting +that has already been done. + +If any headers have been rewritten during routing, update the spool file to +remember them for all subsequent deliveries. This can be delayed till later if +there is only address to be delivered - if it succeeds the spool write need not +happen. */ + +if ( f.header_rewritten + && ( addr_local && (addr_local->next || addr_remote) + || addr_remote && addr_remote->next + ) ) + { + /* Panic-dies on error */ + (void)spool_write_header(message_id, SW_DELIVERING, NULL); + f.header_rewritten = FALSE; + } + + +/* If there are any deliveries to do and we do not already have the journal +file, create it. This is used to record successful deliveries as soon as +possible after each delivery is known to be complete. A file opened with +O_APPEND is used so that several processes can run simultaneously. + +The journal is just insurance against crashes. When the spool file is +ultimately updated at the end of processing, the journal is deleted. If a +journal is found to exist at the start of delivery, the addresses listed +therein are added to the non-recipients. */ + +if (addr_local || addr_remote) + { + if (journal_fd < 0) + { + uschar * fname = spool_fname(US"input", message_subdir, id, US"-J"); + + if ((journal_fd = Uopen(fname, +#ifdef O_CLOEXEC + O_CLOEXEC | +#endif + O_WRONLY|O_APPEND|O_CREAT|O_EXCL, SPOOL_MODE)) < 0) + { + log_write(0, LOG_MAIN|LOG_PANIC, "Couldn't open journal file %s: %s", + fname, strerror(errno)); + return DELIVER_NOT_ATTEMPTED; + } + + /* Set the close-on-exec flag, make the file owned by Exim, and ensure + that the mode is correct - the group setting doesn't always seem to get + set automatically. */ + + if( exim_fchown(journal_fd, exim_uid, exim_gid, fname) + || fchmod(journal_fd, SPOOL_MODE) +#ifndef O_CLOEXEC + || fcntl(journal_fd, F_SETFD, fcntl(journal_fd, F_GETFD) | FD_CLOEXEC) +#endif + ) + { + int ret = Uunlink(fname); + log_write(0, LOG_MAIN|LOG_PANIC, "Couldn't set perms on journal file %s: %s", + fname, strerror(errno)); + if(ret && errno != ENOENT) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "failed to unlink %s: %s", + fname, strerror(errno)); + return DELIVER_NOT_ATTEMPTED; + } + } + } +else if (journal_fd >= 0) + { + close(journal_fd); + journal_fd = -1; + } + + + +/* Now we can get down to the business of actually doing deliveries. Local +deliveries are done first, then remote ones. If ever the problems of how to +handle fallback transports are figured out, this section can be put into a loop +for handling fallbacks, though the uid switching will have to be revised. */ + +/* Precompile a regex that is used to recognize a parameter in response +to an LHLO command, if is isn't already compiled. This may be used on both +local and remote LMTP deliveries. */ + +if (!regex_IGNOREQUOTA) + regex_IGNOREQUOTA = + regex_must_compile(US"\\n250[\\s\\-]IGNOREQUOTA(\\s|\\n|$)", FALSE, TRUE); + +/* Handle local deliveries */ + +if (addr_local) + { + DEBUG(D_deliver|D_transport) + debug_printf(">>>>>>>>>>>>>>>> Local deliveries >>>>>>>>>>>>>>>>\n"); + do_local_deliveries(); + f.disable_logging = FALSE; + } + +/* If queue_run_local is set, we do not want to attempt any remote deliveries, +so just queue them all. */ + +if (f.queue_run_local) + while (addr_remote) + { + address_item *addr = addr_remote; + addr_remote = addr->next; + addr->next = NULL; + addr->basic_errno = ERRNO_LOCAL_ONLY; + addr->message = US"remote deliveries suppressed"; + (void)post_process_one(addr, DEFER, LOG_MAIN, EXIM_DTYPE_TRANSPORT, 0); + } + +/* Handle remote deliveries */ + +if (addr_remote) + { + DEBUG(D_deliver|D_transport) + debug_printf(">>>>>>>>>>>>>>>> Remote deliveries >>>>>>>>>>>>>>>>\n"); + + /* Precompile some regex that are used to recognize parameters in response + to an EHLO command, if they aren't already compiled. */ + + smtp_deliver_init(); + + /* Now sort the addresses if required, and do the deliveries. The yield of + do_remote_deliveries is FALSE when mua_wrapper is set and all addresses + cannot be delivered in one transaction. */ + + if (remote_sort_domains) sort_remote_deliveries(); + if (!do_remote_deliveries(FALSE)) + { + log_write(0, LOG_MAIN, "** mua_wrapper is set but recipients cannot all " + "be delivered in one transaction"); + fprintf(stderr, "delivery to smarthost failed (configuration problem)\n"); + + final_yield = DELIVER_MUA_FAILED; + addr_failed = addr_defer = NULL; /* So that we remove the message */ + goto DELIVERY_TIDYUP; + } + + /* See if any of the addresses that failed got put on the queue for delivery + to their fallback hosts. We do it this way because often the same fallback + host is used for many domains, so all can be sent in a single transaction + (if appropriately configured). */ + + if (addr_fallback && !mua_wrapper) + { + DEBUG(D_deliver) debug_printf("Delivering to fallback hosts\n"); + addr_remote = addr_fallback; + addr_fallback = NULL; + if (remote_sort_domains) sort_remote_deliveries(); + do_remote_deliveries(TRUE); + } + f.disable_logging = FALSE; + } + + +/* All deliveries are now complete. Ignore SIGTERM during this tidying up +phase, to minimize cases of half-done things. */ + +DEBUG(D_deliver) + debug_printf(">>>>>>>>>>>>>>>> deliveries are done >>>>>>>>>>>>>>>>\n"); +cancel_cutthrough_connection(TRUE, US"deliveries are done"); + +/* Root privilege is no longer needed */ + +exim_setugid(exim_uid, exim_gid, FALSE, US"post-delivery tidying"); + +set_process_info("tidying up after delivering %s", message_id); +signal(SIGTERM, SIG_IGN); + +/* When we are acting as an MUA wrapper, the smtp transport will either have +succeeded for all addresses, or failed them all in normal cases. However, there +are some setup situations (e.g. when a named port does not exist) that cause an +immediate exit with deferral of all addresses. Convert those into failures. We +do not ever want to retry, nor do we want to send a bounce message. */ + +if (mua_wrapper) + { + if (addr_defer) + { + address_item * nextaddr; + for (address_item * addr = addr_defer; addr; addr = nextaddr) + { + log_write(0, LOG_MAIN, "** %s mua_wrapper forced failure for deferred " + "delivery", addr->address); + nextaddr = addr->next; + addr->next = addr_failed; + addr_failed = addr; + } + addr_defer = NULL; + } + + /* Now all should either have succeeded or failed. */ + + if (!addr_failed) + final_yield = DELIVER_MUA_SUCCEEDED; + else + { + host_item * host; + uschar *s = addr_failed->user_message; + + if (!s) s = addr_failed->message; + + fprintf(stderr, "Delivery failed: "); + if (addr_failed->basic_errno > 0) + { + fprintf(stderr, "%s", strerror(addr_failed->basic_errno)); + if (s) fprintf(stderr, ": "); + } + if ((host = addr_failed->host_used)) + fprintf(stderr, "H=%s [%s]: ", host->name, host->address); + if (s) + fprintf(stderr, "%s", CS s); + else if (addr_failed->basic_errno <= 0) + fprintf(stderr, "unknown error"); + fprintf(stderr, "\n"); + + final_yield = DELIVER_MUA_FAILED; + addr_failed = NULL; + } + } + +/* In a normal configuration, we now update the retry database. This is done in +one fell swoop at the end in order not to keep opening and closing (and +locking) the database. The code for handling retries is hived off into a +separate module for convenience. We pass it the addresses of the various +chains, because deferred addresses can get moved onto the failed chain if the +retry cutoff time has expired for all alternative destinations. Bypass the +updating of the database if the -N flag is set, which is a debugging thing that +prevents actual delivery. */ + +else if (!f.dont_deliver) + retry_update(&addr_defer, &addr_failed, &addr_succeed); + +/* Send DSN for successful messages if requested */ +addr_senddsn = NULL; + +for (address_item * a = addr_succeed; a; a = a->next) + { + /* af_ignore_error not honored here. it's not an error */ + DEBUG(D_deliver) debug_printf("DSN: processing router : %s\n" + "DSN: processing successful delivery address: %s\n" + "DSN: Sender_address: %s\n" + "DSN: orcpt: %s flags: 0x%x\n" + "DSN: envid: %s ret: %d\n" + "DSN: Final recipient: %s\n" + "DSN: Remote SMTP server supports DSN: %d\n", + a->router ? a->router->name : US"(unknown)", + a->address, + sender_address, + a->dsn_orcpt ? a->dsn_orcpt : US"NULL", + a->dsn_flags, + dsn_envid ? dsn_envid : US"NULL", dsn_ret, + a->address, + a->dsn_aware + ); + + /* send report if next hop not DSN aware or a router flagged "last DSN hop" + and a report was requested */ + + if ( (a->dsn_aware != dsn_support_yes || a->dsn_flags & rf_dsnlasthop) + && a->dsn_flags & rf_notify_success + ) + { + /* copy and relink address_item and send report with all of them at once later */ + address_item * addr_next = addr_senddsn; + addr_senddsn = store_get(sizeof(address_item), GET_UNTAINTED); + *addr_senddsn = *a; + addr_senddsn->next = addr_next; + } + else + DEBUG(D_deliver) debug_printf("DSN: not sending DSN success message\n"); + } + +if (addr_senddsn) + { + pid_t pid; + int fd; + + /* create exim process to send message */ + pid = child_open_exim(&fd, US"DSN"); + + DEBUG(D_deliver) debug_printf("DSN: child_open_exim returns: %d\n", pid); + + if (pid < 0) /* Creation of child failed */ + { + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "Process %d (parent %d) failed to " + "create child process to send success-dsn message: %s", getpid(), + getppid(), strerror(errno)); + + DEBUG(D_deliver) debug_printf("DSN: child_open_exim failed\n"); + } + else /* Creation of child succeeded */ + { + FILE * f = fdopen(fd, "wb"); + /* header only as required by RFC. only failure DSN needs to honor RET=FULL */ + uschar * bound; + transport_ctx tctx = {{0}}; + + DEBUG(D_deliver) + debug_printf("sending success-dsn to: %s\n", sender_address); + + /* build unique id for MIME boundary */ + bound = string_sprintf(TIME_T_FMT "-eximdsn-%d", time(NULL), rand()); + DEBUG(D_deliver) debug_printf("DSN: MIME boundary: %s\n", bound); + + if (errors_reply_to) + fprintf(f, "Reply-To: %s\n", errors_reply_to); + + moan_write_from(f); + fprintf(f, "Auto-Submitted: auto-generated\n" + "To: %s\n" + "Subject: Delivery Status Notification\n", + sender_address); + moan_write_references(f, NULL); + fprintf(f, "Content-Type: multipart/report;" + " report-type=delivery-status; boundary=%s\n" + "MIME-Version: 1.0\n\n" + + "--%s\n" + "Content-type: text/plain; charset=us-ascii\n\n" + + "This message was created automatically by mail delivery software.\n" + " ----- The following addresses had successful delivery notifications -----\n", + bound, bound); + + for (address_item * a = addr_senddsn; a; a = a->next) + fprintf(f, "<%s> (relayed %s)\n\n", + a->address, + a->dsn_flags & rf_dsnlasthop ? "via non DSN router" + : a->dsn_aware == dsn_support_no ? "to non-DSN-aware mailer" + : "via non \"Remote SMTP\" router" + ); + + fprintf(f, "--%s\n" + "Content-type: message/delivery-status\n\n" + "Reporting-MTA: dns; %s\n", + bound, smtp_active_hostname); + + if (dsn_envid) + { /* must be decoded from xtext: see RFC 3461:6.3a */ + uschar *xdec_envid; + if (auth_xtextdecode(dsn_envid, &xdec_envid) > 0) + fprintf(f, "Original-Envelope-ID: %s\n", dsn_envid); + else + fprintf(f, "X-Original-Envelope-ID: error decoding xtext formatted ENVID\n"); + } + fputc('\n', f); + + for (address_item * a = addr_senddsn; a; a = a->next) + { + host_item * hu; + + print_dsn_addr_action(f, a, US"delivered", US"2.0.0"); + + if ((hu = a->host_used) && hu->name) + fprintf(f, "Remote-MTA: dns; %s\nDiagnostic-Code: smtp; 250 Ok\n\n", + hu->name); + else + fprintf(f, "Diagnostic-Code: X-Exim; relayed via non %s router\n\n", + a->dsn_flags & rf_dsnlasthop ? "DSN" : "SMTP"); + } + + fprintf(f, "--%s\nContent-type: text/rfc822-headers\n\n", bound); + + fflush(f); + transport_filter_argv = NULL; /* Just in case */ + return_path = sender_address; /* In case not previously set */ + + /* Write the original email out */ + + tctx.u.fd = fd; + tctx.options = topt_add_return_path | topt_no_body; + /*XXX hmm, FALSE(fail) retval ignored. + Could error for any number of reasons, and they are not handled. */ + transport_write_message(&tctx, 0); + fflush(f); + + fprintf(f,"\n--%s--\n", bound); + + fflush(f); + fclose(f); + rc = child_close(pid, 0); /* Waits for child to close, no timeout */ + } + } + +/* If any addresses failed, we must send a message to somebody, unless +af_ignore_error is set, in which case no action is taken. It is possible for +several messages to get sent if there are addresses with different +requirements. */ + +while (addr_failed) + { + pid_t pid; + int fd; + uschar *logtod = tod_stamp(tod_log); + address_item *addr; + address_item *handled_addr = NULL; + address_item **paddr; + address_item *msgchain = NULL; + address_item **pmsgchain = &msgchain; + + /* There are weird cases when logging is disabled in the transport. However, + there may not be a transport (address failed by a router). */ + + f.disable_logging = FALSE; + if (addr_failed->transport) + f.disable_logging = addr_failed->transport->disable_logging; + + DEBUG(D_deliver) + debug_printf("processing failed address %s\n", addr_failed->address); + + /* There are only two ways an address in a bounce message can get here: + + (1) When delivery was initially deferred, but has now timed out (in the call + to retry_update() above). We can detect this by testing for + af_retry_timedout. If the address does not have its own errors address, + we arrange to ignore the error. + + (2) If delivery failures for bounce messages are being ignored. We can detect + this by testing for af_ignore_error. This will also be set if a bounce + message has been autothawed and the ignore_bounce_errors_after time has + passed. It might also be set if a router was explicitly configured to + ignore errors (errors_to = ""). + + If neither of these cases obtains, something has gone wrong. Log the + incident, but then ignore the error. */ + + if (sender_address[0] == 0 && !addr_failed->prop.errors_address) + { + if ( !testflag(addr_failed, af_retry_timedout) + && !addr_failed->prop.ignore_error) + log_write(0, LOG_MAIN|LOG_PANIC, "internal error: bounce message " + "failure is neither frozen nor ignored (it's been ignored)"); + + addr_failed->prop.ignore_error = TRUE; + } + + /* If the first address on the list has af_ignore_error set, just remove + it from the list, throw away any saved message file, log it, and + mark the recipient done. */ + + if ( addr_failed->prop.ignore_error + || addr_failed->dsn_flags & rf_dsnflags + && !(addr_failed->dsn_flags & rf_notify_failure) + ) + { + addr = addr_failed; + addr_failed = addr->next; + if (addr->return_filename) Uunlink(addr->return_filename); + +#ifndef DISABLE_EVENT + msg_event_raise(US"msg:fail:delivery", addr); +#endif + log_write(0, LOG_MAIN, "%s%s%s%s: error ignored%s", + addr->address, + !addr->parent ? US"" : US" <", + !addr->parent ? US"" : addr->parent->address, + !addr->parent ? US"" : US">", + addr->prop.ignore_error + ? US"" : US": RFC 3461 DSN, failure notify not requested"); + + address_done(addr, logtod); + child_done(addr, logtod); + /* Panic-dies on error */ + (void)spool_write_header(message_id, SW_DELIVERING, NULL); + } + + /* Otherwise, handle the sending of a message. Find the error address for + the first address, then send a message that includes all failed addresses + that have the same error address. Note the bounce_recipient is a global so + that it can be accessed by $bounce_recipient while creating a customized + error message. */ + + else + { + if (!(bounce_recipient = addr_failed->prop.errors_address)) + bounce_recipient = sender_address; + + /* Make a subprocess to send a message */ + + if ((pid = child_open_exim(&fd, US"bounce-message")) < 0) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "Process %d (parent %d) failed to " + "create child process to send failure message: %s", getpid(), + getppid(), strerror(errno)); + + /* Creation of child succeeded */ + + else + { + int ch, rc; + int filecount = 0; + int rcount = 0; + uschar *bcc, *emf_text; + FILE * fp = fdopen(fd, "wb"); + FILE * emf = NULL; + BOOL to_sender = strcmpic(sender_address, bounce_recipient) == 0; + int max = (bounce_return_size_limit/DELIVER_IN_BUFFER_SIZE + 1) * + DELIVER_IN_BUFFER_SIZE; + uschar * bound; + uschar *dsnlimitmsg; + uschar *dsnnotifyhdr; + int topt; + + DEBUG(D_deliver) + debug_printf("sending error message to: %s\n", bounce_recipient); + + /* Scan the addresses for all that have the same errors address, removing + them from the addr_failed chain, and putting them on msgchain. */ + + paddr = &addr_failed; + for (addr = addr_failed; addr; addr = *paddr) + if (Ustrcmp(bounce_recipient, addr->prop.errors_address + ? addr->prop.errors_address : sender_address) == 0) + { /* The same - dechain */ + *paddr = addr->next; + *pmsgchain = addr; + addr->next = NULL; + pmsgchain = &(addr->next); + } + else + paddr = &addr->next; /* Not the same; skip */ + + /* Include X-Failed-Recipients: for automatic interpretation, but do + not let any one header line get too long. We do this by starting a + new header every 50 recipients. Omit any addresses for which the + "hide_child" flag is set. */ + + for (addr = msgchain; addr; addr = addr->next) + { + if (testflag(addr, af_hide_child)) continue; + if (rcount >= 50) + { + fprintf(fp, "\n"); + rcount = 0; + } + fprintf(fp, "%s%s", + rcount++ == 0 + ? "X-Failed-Recipients: " + : ",\n ", + testflag(addr, af_pfr) && addr->parent + ? string_printing(addr->parent->address) + : string_printing(addr->address)); + } + if (rcount > 0) fprintf(fp, "\n"); + + /* Output the standard headers */ + + if (errors_reply_to) + fprintf(fp, "Reply-To: %s\n", errors_reply_to); + fprintf(fp, "Auto-Submitted: auto-replied\n"); + moan_write_from(fp); + fprintf(fp, "To: %s\n", bounce_recipient); + moan_write_references(fp, NULL); + + /* generate boundary string and output MIME-Headers */ + bound = string_sprintf(TIME_T_FMT "-eximdsn-%d", time(NULL), rand()); + + fprintf(fp, "Content-Type: multipart/report;" + " report-type=delivery-status; boundary=%s\n" + "MIME-Version: 1.0\n", + bound); + + /* Open a template file if one is provided. Log failure to open, but + carry on - default texts will be used. */ + + if (bounce_message_file) + emf = expand_open(bounce_message_file, + US"bounce_message_file", US"error"); + + /* Quietly copy to configured additional addresses if required. */ + + if ((bcc = moan_check_errorcopy(bounce_recipient))) + fprintf(fp, "Bcc: %s\n", bcc); + + /* The texts for the message can be read from a template file; if there + isn't one, or if it is too short, built-in texts are used. The first + emf text is a Subject: and any other headers. */ + + if ((emf_text = next_emf(emf, US"header"))) + fprintf(fp, "%s\n", emf_text); + else + fprintf(fp, "Subject: Mail delivery failed%s\n\n", + to_sender? ": returning message to sender" : ""); + + /* output human readable part as text/plain section */ + fprintf(fp, "--%s\n" + "Content-type: text/plain; charset=us-ascii\n\n", + bound); + + if ((emf_text = next_emf(emf, US"intro"))) + fprintf(fp, "%s", CS emf_text); + else + { + fprintf(fp, +/* This message has been reworded several times. It seems to be confusing to +somebody, however it is worded. I have retreated to the original, simple +wording. */ +"This message was created automatically by mail delivery software.\n"); + + if (bounce_message_text) + fprintf(fp, "%s", CS bounce_message_text); + if (to_sender) + fprintf(fp, +"\nA message that you sent could not be delivered to one or more of its\n" +"recipients. This is a permanent error. The following address(es) failed:\n"); + else + fprintf(fp, +"\nA message sent by\n\n <%s>\n\n" +"could not be delivered to one or more of its recipients. The following\n" +"address(es) failed:\n", sender_address); + } + fputc('\n', fp); + + /* Process the addresses, leaving them on the msgchain if they have a + file name for a return message. (There has already been a check in + post_process_one() for the existence of data in the message file.) A TRUE + return from print_address_information() means that the address is not + hidden. */ + + paddr = &msgchain; + for (addr = msgchain; addr; addr = *paddr) + { + if (print_address_information(addr, fp, US" ", US"\n ", US"")) + print_address_error(addr, fp, US""); + + /* End the final line for the address */ + + fputc('\n', fp); + + /* Leave on msgchain if there's a return file. */ + + if (addr->return_file >= 0) + { + paddr = &(addr->next); + filecount++; + } + + /* Else save so that we can tick off the recipient when the + message is sent. */ + + else + { + *paddr = addr->next; + addr->next = handled_addr; + handled_addr = addr; + } + } + + fputc('\n', fp); + + /* Get the next text, whether we need it or not, so as to be + positioned for the one after. */ + + emf_text = next_emf(emf, US"generated text"); + + /* If there were any file messages passed by the local transports, + include them in the message. Then put the address on the handled chain. + In the case of a batch of addresses that were all sent to the same + transport, the return_file field in all of them will contain the same + fd, and the return_filename field in the *last* one will be set (to the + name of the file). */ + + if (msgchain) + { + address_item *nextaddr; + + if (emf_text) + fprintf(fp, "%s", CS emf_text); + else + fprintf(fp, + "The following text was generated during the delivery " + "attempt%s:\n", (filecount > 1)? "s" : ""); + + for (addr = msgchain; addr; addr = nextaddr) + { + FILE *fm; + address_item *topaddr = addr; + + /* List all the addresses that relate to this file */ + + fputc('\n', fp); + while(addr) /* Insurance */ + { + print_address_information(addr, fp, US"------ ", US"\n ", + US" ------\n"); + if (addr->return_filename) break; + addr = addr->next; + } + fputc('\n', fp); + + /* Now copy the file */ + + if (!(fm = Ufopen(addr->return_filename, "rb"))) + fprintf(fp, " +++ Exim error... failed to open text file: %s\n", + strerror(errno)); + else + { + while ((ch = fgetc(fm)) != EOF) fputc(ch, fp); + (void)fclose(fm); + } + Uunlink(addr->return_filename); + + /* Can now add to handled chain, first fishing off the next + address on the msgchain. */ + + nextaddr = addr->next; + addr->next = handled_addr; + handled_addr = topaddr; + } + fputc('\n', fp); + } + + /* output machine readable part */ +#ifdef SUPPORT_I18N + if (message_smtputf8) + fprintf(fp, "--%s\n" + "Content-type: message/global-delivery-status\n\n" + "Reporting-MTA: dns; %s\n", + bound, smtp_active_hostname); + else +#endif + fprintf(fp, "--%s\n" + "Content-type: message/delivery-status\n\n" + "Reporting-MTA: dns; %s\n", + bound, smtp_active_hostname); + + if (dsn_envid) + { + /* must be decoded from xtext: see RFC 3461:6.3a */ + uschar *xdec_envid; + if (auth_xtextdecode(dsn_envid, &xdec_envid) > 0) + fprintf(fp, "Original-Envelope-ID: %s\n", dsn_envid); + else + fprintf(fp, "X-Original-Envelope-ID: error decoding xtext formatted ENVID\n"); + } + fputc('\n', fp); + + for (addr = handled_addr; addr; addr = addr->next) + { + host_item * hu; + + print_dsn_addr_action(fp, addr, US"failed", US"5.0.0"); + + if ((hu = addr->host_used) && hu->name) + { + fprintf(fp, "Remote-MTA: dns; %s\n", hu->name); +#ifdef EXPERIMENTAL_DSN_INFO + { + const uschar * s; + if (hu->address) + { + uschar * p = hu->port == 25 + ? US"" : string_sprintf(":%d", hu->port); + fprintf(fp, "Remote-MTA: X-ip; [%s]%s\n", hu->address, p); + } + if ((s = addr->smtp_greeting) && *s) + fprintf(fp, "X-Remote-MTA-smtp-greeting: X-str; %.900s\n", s); + if ((s = addr->helo_response) && *s) + fprintf(fp, "X-Remote-MTA-helo-response: X-str; %.900s\n", s); + if ((s = addr->message) && *s) + fprintf(fp, "X-Exim-Diagnostic: X-str; %.900s\n", s); + } +#endif + print_dsn_diagnostic_code(addr, fp); + } + fputc('\n', fp); + } + + /* Now copy the message, trying to give an intelligible comment if + it is too long for it all to be copied. The limit isn't strictly + applied because of the buffering. There is, however, an option + to suppress copying altogether. */ + + emf_text = next_emf(emf, US"copy"); + + /* add message body + we ignore the intro text from template and add + the text for bounce_return_size_limit at the end. + + bounce_return_message is ignored + in case RET= is defined we honor these values + otherwise bounce_return_body is honored. + + bounce_return_size_limit is always honored. + */ + + fprintf(fp, "--%s\n", bound); + + dsnlimitmsg = US"X-Exim-DSN-Information: Due to administrative limits only headers are returned"; + dsnnotifyhdr = NULL; + topt = topt_add_return_path; + + /* RET=HDRS? top priority */ + if (dsn_ret == dsn_ret_hdrs) + topt |= topt_no_body; + else + { + struct stat statbuf; + + /* no full body return at all? */ + if (!bounce_return_body) + { + topt |= topt_no_body; + /* add header if we overrule RET=FULL */ + if (dsn_ret == dsn_ret_full) + dsnnotifyhdr = dsnlimitmsg; + } + /* line length limited... return headers only if oversize */ + /* size limited ... return headers only if limit reached */ + else if ( max_received_linelength > bounce_return_linesize_limit + || ( bounce_return_size_limit > 0 + && fstat(deliver_datafile, &statbuf) == 0 + && statbuf.st_size > max + ) ) + { + topt |= topt_no_body; + dsnnotifyhdr = dsnlimitmsg; + } + } + +#ifdef SUPPORT_I18N + if (message_smtputf8) + fputs(topt & topt_no_body ? "Content-type: message/global-headers\n\n" + : "Content-type: message/global\n\n", + fp); + else +#endif + fputs(topt & topt_no_body ? "Content-type: text/rfc822-headers\n\n" + : "Content-type: message/rfc822\n\n", + fp); + + fflush(fp); + transport_filter_argv = NULL; /* Just in case */ + return_path = sender_address; /* In case not previously set */ + { /* Dummy transport for headers add */ + transport_ctx tctx = {{0}}; + transport_instance tb = {0}; + + tctx.u.fd = fileno(fp); + tctx.tblock = &tb; + tctx.options = topt; + tb.add_headers = dsnnotifyhdr; + + /*XXX no checking for failure! buggy! */ + transport_write_message(&tctx, 0); + } + fflush(fp); + + /* we never add the final text. close the file */ + if (emf) + (void)fclose(emf); + + fprintf(fp, "\n--%s--\n", bound); + + /* Close the file, which should send an EOF to the child process + that is receiving the message. Wait for it to finish. */ + + (void)fclose(fp); + rc = child_close(pid, 0); /* Waits for child to close, no timeout */ + + /* If the process failed, there was some disaster in setting up the + error message. Unless the message is very old, ensure that addr_defer + is non-null, which will have the effect of leaving the message on the + spool. The failed addresses will get tried again next time. However, we + don't really want this to happen too often, so freeze the message unless + there are some genuine deferred addresses to try. To do this we have + to call spool_write_header() here, because with no genuine deferred + addresses the normal code below doesn't get run. */ + + if (rc != 0) + { + uschar *s = US""; + if (now - received_time.tv_sec < retry_maximum_timeout && !addr_defer) + { + addr_defer = (address_item *)(+1); + f.deliver_freeze = TRUE; + deliver_frozen_at = time(NULL); + /* Panic-dies on error */ + (void)spool_write_header(message_id, SW_DELIVERING, NULL); + s = US" (frozen)"; + } + deliver_msglog("Process failed (%d) when writing error message " + "to %s%s", rc, bounce_recipient, s); + log_write(0, LOG_MAIN, "Process failed (%d) when writing error message " + "to %s%s", rc, bounce_recipient, s); + } + + /* The message succeeded. Ensure that the recipients that failed are + now marked finished with on the spool and their parents updated. */ + + else + { + for (addr = handled_addr; addr; addr = addr->next) + { + address_done(addr, logtod); + child_done(addr, logtod); + } + /* Panic-dies on error */ + (void)spool_write_header(message_id, SW_DELIVERING, NULL); + } + } + } + } + +f.disable_logging = FALSE; /* In case left set */ + +/* Come here from the mua_wrapper case if routing goes wrong */ + +DELIVERY_TIDYUP: + +/* If there are now no deferred addresses, we are done. Preserve the +message log if so configured, and we are using them. Otherwise, sling it. +Then delete the message itself. */ + +if (!addr_defer) + { + uschar * fname; + + if (message_logs) + { + fname = spool_fname(US"msglog", message_subdir, id, US""); + if (preserve_message_logs) + { + int rc; + uschar * moname = spool_fname(US"msglog.OLD", US"", id, US""); + + if ((rc = Urename(fname, moname)) < 0) + { + (void)directory_make(spool_directory, + spool_sname(US"msglog.OLD", US""), + MSGLOG_DIRECTORY_MODE, TRUE); + rc = Urename(fname, moname); + } + if (rc < 0) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "failed to move %s to the " + "msglog.OLD directory", fname); + } + else + if (Uunlink(fname) < 0) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "failed to unlink %s: %s", + fname, strerror(errno)); + } + + /* Remove the two message files. */ + + fname = spool_fname(US"input", message_subdir, id, US"-D"); + if (Uunlink(fname) < 0) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "failed to unlink %s: %s", + fname, strerror(errno)); + fname = spool_fname(US"input", message_subdir, id, US"-H"); + if (Uunlink(fname) < 0) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "failed to unlink %s: %s", + fname, strerror(errno)); + + /* Log the end of this message, with queue time if requested. */ + + if (LOGGING(queue_time_overall)) + log_write(0, LOG_MAIN, "Completed QT=%s", string_timesince(&received_time)); + else + log_write(0, LOG_MAIN, "Completed"); + + /* Unset deliver_freeze so that we won't try to move the spool files further down */ + f.deliver_freeze = FALSE; + +#ifndef DISABLE_EVENT + (void) event_raise(event_action, US"msg:complete", NULL, NULL); +#endif + } + +/* If there are deferred addresses, we are keeping this message because it is +not yet completed. Lose any temporary files that were catching output from +pipes for any of the deferred addresses, handle one-time aliases, and see if +the message has been on the queue for so long that it is time to send a warning +message to the sender, unless it is a mailer-daemon. If all deferred addresses +have the same domain, we can set deliver_domain for the expansion of +delay_warning_ condition - if any of them are pipes, files, or autoreplies, use +the parent's domain. + +If all the deferred addresses have an error number that indicates "retry time +not reached", skip sending the warning message, because it won't contain the +reason for the delay. It will get sent at the next real delivery attempt. + Exception: for retries caused by a remote peer we use the error message + store in the retry DB as the reason. +However, if at least one address has tried, we'd better include all of them in +the message. + +If we can't make a process to send the message, don't worry. + +For mailing list expansions we want to send the warning message to the +mailing list manager. We can't do a perfect job here, as some addresses may +have different errors addresses, but if we take the errors address from +each deferred address it will probably be right in most cases. + +If addr_defer == +1, it means there was a problem sending an error message +for failed addresses, and there were no "real" deferred addresses. The value +was set just to keep the message on the spool, so there is nothing to do here. +*/ + +else if (addr_defer != (address_item *)(+1)) + { + uschar *recipients = US""; + BOOL want_warning_msg = FALSE; + + deliver_domain = testflag(addr_defer, af_pfr) + ? addr_defer->parent->domain : addr_defer->domain; + + for (address_item * addr = addr_defer; addr; addr = addr->next) + { + address_item *otaddr; + + if (addr->basic_errno > ERRNO_WARN_BASE) want_warning_msg = TRUE; + + if (deliver_domain) + { + const uschar *d = testflag(addr, af_pfr) + ? addr->parent->domain : addr->domain; + + /* The domain may be unset for an address that has never been routed + because the system filter froze the message. */ + + if (!d || Ustrcmp(d, deliver_domain) != 0) + deliver_domain = NULL; + } + + if (addr->return_filename) Uunlink(addr->return_filename); + + /* Handle the case of one-time aliases. If any address in the ancestry + of this one is flagged, ensure it is in the recipients list, suitably + flagged, and that its parent is marked delivered. */ + + for (otaddr = addr; otaddr; otaddr = otaddr->parent) + if (otaddr->onetime_parent) break; + + if (otaddr) + { + int i; + int t = recipients_count; + + for (i = 0; i < recipients_count; i++) + { + uschar *r = recipients_list[i].address; + if (Ustrcmp(otaddr->onetime_parent, r) == 0) t = i; + if (Ustrcmp(otaddr->address, r) == 0) break; + } + + /* Didn't find the address already in the list, and did find the + ultimate parent's address in the list, and they really are different + (i.e. not from an identity-redirect). After adding the recipient, + update the errors address in the recipients list. */ + + if ( i >= recipients_count && t < recipients_count + && Ustrcmp(otaddr->address, otaddr->parent->address) != 0) + { + DEBUG(D_deliver) debug_printf("one_time: adding %s in place of %s\n", + otaddr->address, otaddr->parent->address); + receive_add_recipient(otaddr->address, t); + recipients_list[recipients_count-1].errors_to = otaddr->prop.errors_address; + tree_add_nonrecipient(otaddr->parent->address); + update_spool = TRUE; + } + } + + /* Except for error messages, ensure that either the errors address for + this deferred address or, if there is none, the sender address, is on the + list of recipients for a warning message. */ + + if (sender_address[0]) + { + uschar * s = addr->prop.errors_address; + if (!s) s = sender_address; + if (Ustrstr(recipients, s) == NULL) + recipients = string_sprintf("%s%s%s", recipients, + recipients[0] ? "," : "", s); + } + } + + /* Send a warning message if the conditions are right. If the condition check + fails because of a lookup defer, there is nothing we can do. The warning + is not sent. Another attempt will be made at the next delivery attempt (if + it also defers). */ + + if ( !f.queue_2stage + && want_warning_msg + && ( !(addr_defer->dsn_flags & rf_dsnflags) + || addr_defer->dsn_flags & rf_notify_delay + ) + && delay_warning[1] > 0 + && sender_address[0] != 0 + && ( !delay_warning_condition + || expand_check_condition(delay_warning_condition, + US"delay_warning", US"option") + ) + ) + { + int count; + int show_time; + int queue_time = time(NULL) - received_time.tv_sec; + + queue_time = test_harness_fudged_queue_time(queue_time); + + /* See how many warnings we should have sent by now */ + + for (count = 0; count < delay_warning[1]; count++) + if (queue_time < delay_warning[count+2]) break; + + show_time = delay_warning[count+1]; + + if (count >= delay_warning[1]) + { + int extra; + int last_gap = show_time; + if (count > 1) last_gap -= delay_warning[count]; + extra = (queue_time - delay_warning[count+1])/last_gap; + show_time += last_gap * extra; + count += extra; + } + + DEBUG(D_deliver) + { + debug_printf("time on queue = %s id %s addr %s\n", + readconf_printtime(queue_time), message_id, addr_defer->address); + debug_printf("warning counts: required %d done %d\n", count, + warning_count); + } + + /* We have computed the number of warnings there should have been by now. + If there haven't been enough, send one, and up the count to what it should + have been. */ + + if (warning_count < count) + { + header_line *h; + int fd; + pid_t pid = child_open_exim(&fd, US"delay-warning-message"); + + if (pid > 0) + { + uschar * wmf_text; + FILE * wmf = NULL; + FILE * f = fdopen(fd, "wb"); + uschar * bound; + transport_ctx tctx = {{0}}; + + if (warn_message_file) + wmf = expand_open(warn_message_file, + US"warn_message_file", US"warning"); + + warnmsg_recipients = recipients; + warnmsg_delay = queue_time < 120*60 + ? string_sprintf("%d minutes", show_time/60) + : string_sprintf("%d hours", show_time/3600); + + if (errors_reply_to) + fprintf(f, "Reply-To: %s\n", errors_reply_to); + fprintf(f, "Auto-Submitted: auto-replied\n"); + moan_write_from(f); + fprintf(f, "To: %s\n", recipients); + moan_write_references(f, NULL); + + /* generated boundary string and output MIME-Headers */ + bound = string_sprintf(TIME_T_FMT "-eximdsn-%d", time(NULL), rand()); + + fprintf(f, "Content-Type: multipart/report;" + " report-type=delivery-status; boundary=%s\n" + "MIME-Version: 1.0\n", + bound); + + if ((wmf_text = next_emf(wmf, US"header"))) + fprintf(f, "%s\n", wmf_text); + else + fprintf(f, "Subject: Warning: message %s delayed %s\n\n", + message_id, warnmsg_delay); + + /* output human readable part as text/plain section */ + fprintf(f, "--%s\n" + "Content-type: text/plain; charset=us-ascii\n\n", + bound); + + if ((wmf_text = next_emf(wmf, US"intro"))) + fprintf(f, "%s", CS wmf_text); + else + { + fprintf(f, +"This message was created automatically by mail delivery software.\n"); + + if (Ustrcmp(recipients, sender_address) == 0) + fprintf(f, +"A message that you sent has not yet been delivered to one or more of its\n" +"recipients after more than "); + + else + fprintf(f, +"A message sent by\n\n <%s>\n\n" +"has not yet been delivered to one or more of its recipients after more than \n", + sender_address); + + fprintf(f, "%s on the queue on %s.\n\n" + "The message identifier is: %s\n", + warnmsg_delay, primary_hostname, message_id); + + for (h = header_list; h; h = h->next) + if (strncmpic(h->text, US"Subject:", 8) == 0) + fprintf(f, "The subject of the message is: %s", h->text + 9); + else if (strncmpic(h->text, US"Date:", 5) == 0) + fprintf(f, "The date of the message is: %s", h->text + 6); + fputc('\n', f); + + fprintf(f, "The address%s to which the message has not yet been " + "delivered %s:\n", + !addr_defer->next ? "" : "es", + !addr_defer->next ? "is": "are"); + } + + /* List the addresses, with error information if allowed */ + + fputc('\n', f); + for (address_item * addr = addr_defer; addr; addr = addr->next) + { + if (print_address_information(addr, f, US" ", US"\n ", US"")) + print_address_error(addr, f, US"Delay reason: "); + fputc('\n', f); + } + fputc('\n', f); + + /* Final text */ + + if (wmf) + { + if ((wmf_text = next_emf(wmf, US"final"))) + fprintf(f, "%s", CS wmf_text); + (void)fclose(wmf); + } + else + { + fprintf(f, +"No action is required on your part. Delivery attempts will continue for\n" +"some time, and this warning may be repeated at intervals if the message\n" +"remains undelivered. Eventually the mail delivery software will give up,\n" +"and when that happens, the message will be returned to you.\n"); + } + + /* output machine readable part */ + fprintf(f, "\n--%s\n" + "Content-type: message/delivery-status\n\n" + "Reporting-MTA: dns; %s\n", + bound, + smtp_active_hostname); + + + if (dsn_envid) + { + /* must be decoded from xtext: see RFC 3461:6.3a */ + uschar *xdec_envid; + if (auth_xtextdecode(dsn_envid, &xdec_envid) > 0) + fprintf(f,"Original-Envelope-ID: %s\n", dsn_envid); + else + fprintf(f,"X-Original-Envelope-ID: error decoding xtext formatted ENVID\n"); + } + fputc('\n', f); + + for (address_item * addr = addr_defer; addr; addr = addr->next) + { + host_item * hu; + + print_dsn_addr_action(f, addr, US"delayed", US"4.0.0"); + + if ((hu = addr->host_used) && hu->name) + { + fprintf(f, "Remote-MTA: dns; %s\n", hu->name); + print_dsn_diagnostic_code(addr, f); + } + fputc('\n', f); + } + + fprintf(f, "--%s\n" + "Content-type: text/rfc822-headers\n\n", + bound); + + fflush(f); + /* header only as required by RFC. only failure DSN needs to honor RET=FULL */ + tctx.u.fd = fileno(f); + tctx.options = topt_add_return_path | topt_no_body; + transport_filter_argv = NULL; /* Just in case */ + return_path = sender_address; /* In case not previously set */ + + /* Write the original email out */ + /*XXX no checking for failure! buggy! */ + transport_write_message(&tctx, 0); + fflush(f); + + fprintf(f,"\n--%s--\n", bound); + + fflush(f); + + /* Close and wait for child process to complete, without a timeout. + If there's an error, don't update the count. */ + + (void)fclose(f); + if (child_close(pid, 0) == 0) + { + warning_count = count; + update_spool = TRUE; /* Ensure spool rewritten */ + } + } + } + } + + /* Clear deliver_domain */ + + deliver_domain = NULL; + + /* If this was a first delivery attempt, unset the first time flag, and + ensure that the spool gets updated. */ + + if (f.deliver_firsttime && !f.queue_2stage) + { + f.deliver_firsttime = FALSE; + update_spool = TRUE; + } + + /* If delivery was frozen and freeze_tell is set, generate an appropriate + message, unless the message is a local error message (to avoid loops). Then + log the freezing. If the text in "frozen_info" came from a system filter, + it has been escaped into printing characters so as not to mess up log lines. + For the "tell" message, we turn \n back into newline. Also, insert a newline + near the start instead of the ": " string. */ + + if (f.deliver_freeze) + { + if (freeze_tell && freeze_tell[0] != 0 && !f.local_error_message) + { + uschar *s = string_copy(frozen_info); + uschar *ss = Ustrstr(s, " by the system filter: "); + + if (ss != NULL) + { + ss[21] = '.'; + ss[22] = '\n'; + } + + ss = s; + while (*ss != 0) + { + if (*ss == '\\' && ss[1] == 'n') + { + *ss++ = ' '; + *ss++ = '\n'; + } + else ss++; + } + moan_tell_someone(freeze_tell, addr_defer, US"Message frozen", + "Message %s has been frozen%s.\nThe sender is <%s>.\n", message_id, + s, sender_address); + } + + /* Log freezing just before we update the -H file, to minimize the chance + of a race problem. */ + + deliver_msglog("*** Frozen%s\n", frozen_info); + log_write(0, LOG_MAIN, "Frozen%s", frozen_info); + } + + /* If there have been any updates to the non-recipients list, or other things + that get written to the spool, we must now update the spool header file so + that it has the right information for the next delivery attempt. If there + was more than one address being delivered, the header_change update is done + earlier, in case one succeeds and then something crashes. */ + + DEBUG(D_deliver) + debug_printf("delivery deferred: update_spool=%d header_rewritten=%d\n", + update_spool, f.header_rewritten); + + if (update_spool || f.header_rewritten) + /* Panic-dies on error */ + (void)spool_write_header(message_id, SW_DELIVERING, NULL); + } + +/* Finished with the message log. If the message is complete, it will have +been unlinked or renamed above. */ + +if (message_logs) (void)fclose(message_log); + +/* Now we can close and remove the journal file. Its only purpose is to record +successfully completed deliveries asap so that this information doesn't get +lost if Exim (or the machine) crashes. Forgetting about a failed delivery is +not serious, as trying it again is not harmful. The journal might not be open +if all addresses were deferred at routing or directing. Nevertheless, we must +remove it if it exists (may have been lying around from a crash during the +previous delivery attempt). We don't remove the journal if a delivery +subprocess failed to pass back delivery information; this is controlled by +the remove_journal flag. When the journal is left, we also don't move the +message off the main spool if frozen and the option is set. It should get moved +at the next attempt, after the journal has been inspected. */ + +if (journal_fd >= 0) (void)close(journal_fd); + +if (remove_journal) + { + uschar * fname = spool_fname(US"input", message_subdir, id, US"-J"); + + if (Uunlink(fname) < 0 && errno != ENOENT) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "failed to unlink %s: %s", fname, + strerror(errno)); + + /* Move the message off the spool if requested */ + +#ifdef SUPPORT_MOVE_FROZEN_MESSAGES + if (f.deliver_freeze && move_frozen_messages) + (void)spool_move_message(id, message_subdir, US"", US"F"); +#endif + } + +/* Closing the data file frees the lock; if the file has been unlinked it +will go away. Otherwise the message becomes available for another process +to try delivery. */ + +(void)close(deliver_datafile); +deliver_datafile = -1; +DEBUG(D_deliver) debug_printf("end delivery of %s\n", id); +#ifdef MEASURE_TIMING +report_time_since(×tamp_startup, US"delivery end"); /* testcase 0005 */ +#endif + +/* It is unlikely that there will be any cached resources, since they are +released after routing, and in the delivery subprocesses. However, it's +possible for an expansion for something afterwards (for example, +expand_check_condition) to do a lookup. We must therefore be sure everything is +released. */ + +search_tidyup(); +acl_where = ACL_WHERE_UNKNOWN; +return final_yield; +} + + + +void +tcp_init(void) +{ +#ifdef EXIM_TFO_PROBE +tfo_probe(); +#else +f.tcp_fastopen_ok = TRUE; +#endif +} + + + +/* Called from a commandline, or from the daemon, to do a delivery. +We need to regain privs; do this by exec of the exim binary. */ + +void +delivery_re_exec(int exec_type) +{ +uschar * where; + +if (cutthrough.cctx.sock >= 0 && cutthrough.callout_hold_only) + { + int channel_fd = cutthrough.cctx.sock; + + smtp_peer_options = cutthrough.peer_options; + continue_sequence = 0; + +#ifndef DISABLE_TLS + if (cutthrough.is_tls) + { + int pfd[2], pid; + + smtp_peer_options |= OPTION_TLS; + sending_ip_address = cutthrough.snd_ip; + sending_port = cutthrough.snd_port; + + where = US"socketpair"; + if (socketpair(AF_UNIX, SOCK_STREAM, 0, pfd) != 0) + goto fail; + + where = US"fork"; + testharness_pause_ms(150); + if ((pid = exim_fork(US"tls-proxy-interproc")) < 0) + goto fail; + + if (pid == 0) /* child: will fork again to totally disconnect */ + { + smtp_proxy_tls(cutthrough.cctx.tls_ctx, big_buffer, big_buffer_size, + pfd, 5*60, cutthrough.host.name); + /* does not return */ + } + + close(pfd[0]); + waitpid(pid, NULL, 0); + (void) close(channel_fd); /* release the client socket */ + channel_fd = pfd[1]; + } +#endif + + transport_do_pass_socket(cutthrough.transport, cutthrough.host.name, + cutthrough.host.address, message_id, channel_fd); + } +else + { + cancel_cutthrough_connection(TRUE, US"non-continued delivery"); + (void) child_exec_exim(exec_type, FALSE, NULL, FALSE, 2, US"-Mc", message_id); + } +return; /* compiler quietening; control does not reach here. */ + +#ifndef DISABLE_TLS +fail: + log_write(0, + LOG_MAIN | (exec_type == CEE_EXEC_EXIT ? LOG_PANIC : LOG_PANIC_DIE), + "delivery re-exec %s failed: %s", where, strerror(errno)); + + /* Get here if exec_type == CEE_EXEC_EXIT. + Note: this must be _exit(), not exit(). */ + + _exit(EX_EXECFAILED); +#endif +} + +/* vi: aw ai sw=2 +*/ +/* End of deliver.c */ diff --git a/src/directory.c b/src/directory.c new file mode 100644 index 0000000..1890208 --- /dev/null +++ b/src/directory.c @@ -0,0 +1,95 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2010 - 2022 */ +/* Copyright (c) University of Cambridge 1995 - 2009 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#include "exim.h" + + +/************************************************* +* Attempt to create a directory * +*************************************************/ + +/* All the directories that Exim ever creates for itself are within the spool +directory as defined by spool_directory. We are prepared to create as many as +necessary from that directory downwards, inclusive. However, directory creation +can also be required in appendfile and sieve filters. The making function +therefore has a parent argument, below which the new directories are to go. It +can be NULL if the name is absolute. + +If a non-root uid has been specified for exim, and we are currently running as +root, ensure the directory is owned by the non-root id if the parent is the +spool directory. + +Arguments: + parent parent directory name; if NULL the name must be absolute + name directory name within the parent that we want + mode mode for the new directory + panic if TRUE, panic on failure + +Returns: panic on failure if panic is set; otherwise return FALSE; + TRUE on success. +*/ + +BOOL +directory_make(const uschar *parent, const uschar *name, + int mode, BOOL panic) +{ +BOOL use_chown = parent == spool_directory && geteuid() == root_uid; +uschar * p; +uschar c = 1; +struct stat statbuf; +uschar * path; + +if (is_tainted(name)) + { p = US"create"; path = US name; errno = ERRNO_TAINT; goto bad; } + +if (parent) + { + path = string_sprintf("%s%s%s", parent, US"/", name); + p = path + Ustrlen(parent); + } +else + { + path = string_copy(name); + p = path + 1; + } + +/* Walk the path creating any missing directories */ + +while (c && *p) + { + while (*p && *p != '/') p++; + c = *p; + *p = '\0'; + if (Ustat(path, &statbuf) != 0) + { + if (mkdir(CS path, mode) < 0 && errno != EEXIST) + { p = US"create"; goto bad; } + + /* Set the ownership if necessary. */ + + if (use_chown && exim_chown(path, exim_uid, exim_gid)) + { p = US"set owner on"; goto bad; } + + /* It appears that any mode bits greater than 0777 are ignored by + mkdir(), at least on some operating systems. Therefore, if the mode + contains any such bits, do an explicit mode setting. */ + + if (mode & 0777000) (void) Uchmod(path, mode); + } + *p++ = c; + } + +return TRUE; + +bad: + if (panic) log_write(0, LOG_MAIN|LOG_PANIC_DIE, + "Failed to %s directory \"%s\": %s\n", p, path, exim_errstr(errno)); + return FALSE; +} + +/* End of directory.c */ diff --git a/src/dkim.c b/src/dkim.c new file mode 100644 index 0000000..bb916d2 --- /dev/null +++ b/src/dkim.c @@ -0,0 +1,895 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* Copyright (c) University of Cambridge, 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Code for DKIM support. Other DKIM relevant code is in + receive.c, transport.c and transports/smtp.c */ + +#include "exim.h" + +#ifndef DISABLE_DKIM + +# include "pdkim/pdkim.h" + +# ifdef MACRO_PREDEF +# include "macro_predef.h" + +void +params_dkim(void) +{ +builtin_macro_create_var(US"_DKIM_SIGN_HEADERS", US PDKIM_DEFAULT_SIGN_HEADERS); +builtin_macro_create_var(US"_DKIM_OVERSIGN_HEADERS", US PDKIM_OVERSIGN_HEADERS); +} +# else /*!MACRO_PREDEF*/ + + + +pdkim_ctx dkim_sign_ctx; + +int dkim_verify_oldpool; +pdkim_ctx *dkim_verify_ctx = NULL; +pdkim_signature *dkim_cur_sig = NULL; +static const uschar * dkim_collect_error = NULL; + +#define DKIM_MAX_SIGNATURES 20 + + + +/* Look up the DKIM record in DNS for the given hostname. +Will use the first found if there are multiple. +The return string is tainted, having come from off-site. +*/ + +uschar * +dkim_exim_query_dns_txt(const uschar * name) +{ +dns_answer * dnsa = store_get_dns_answer(); +dns_scan dnss; +rmark reset_point = store_mark(); +gstring * g = string_get_tainted(256, GET_TAINTED); + +lookup_dnssec_authenticated = NULL; +if (dns_lookup(dnsa, name, T_TXT, NULL) != DNS_SUCCEED) + goto bad; + +/* Search for TXT record */ + +for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); + rr; + rr = dns_next_rr(dnsa, &dnss, RESET_NEXT)) + if (rr->type == T_TXT) + { /* Copy record content to the answer buffer */ + for (int rr_offset = 0; rr_offset < rr->size; ) + { + uschar len = rr->data[rr_offset++]; + + g = string_catn(g, US(rr->data + rr_offset), len); + if (g->ptr >= PDKIM_DNS_TXT_MAX_RECLEN) + goto bad; + + rr_offset += len; + } + + /* Check if this looks like a DKIM record */ + if (Ustrncmp(g->s, "v=", 2) != 0 || strncasecmp(CS g->s, "v=dkim", 6) == 0) + { + store_free_dns_answer(dnsa); + gstring_release_unused(g); + return string_from_gstring(g); + } + + g->ptr = 0; /* overwrite previous record */ + } + +bad: +store_reset(reset_point); +store_free_dns_answer(dnsa); +return NULL; /*XXX better error detail? logging? */ +} + + +void +dkim_exim_init(void) +{ +if (f.dkim_init_done) return; +f.dkim_init_done = TRUE; +pdkim_init(); +} + + + +void +dkim_exim_verify_init(BOOL dot_stuffing) +{ +dkim_exim_init(); + +/* There is a store-reset between header & body reception for the main pool +(actually, after every header line) so cannot use that as we need the data we +store per-header, during header processing, at the end of body reception +for evaluating the signature. Any allocs done for dkim verify +memory-handling must use a different pool. We use a separate one that we +can reset per message. */ + +dkim_verify_oldpool = store_pool; +store_pool = POOL_MESSAGE; + +/* Free previous context if there is one */ + +if (dkim_verify_ctx) + pdkim_free_ctx(dkim_verify_ctx); + +/* Create new context */ + +dkim_verify_ctx = pdkim_init_verify(&dkim_exim_query_dns_txt, dot_stuffing); +dkim_collect_input = dkim_verify_ctx ? DKIM_MAX_SIGNATURES : 0; +dkim_collect_error = NULL; + +/* Start feed up with any cached data, but limited to message data */ +receive_get_cache(chunking_state == CHUNKING_LAST + ? chunking_data_left : GETC_BUFFER_UNLIMITED); + +store_pool = dkim_verify_oldpool; +} + + +/* Submit a chunk of data for verification input. +Only use the data when the feed is activated. */ +void +dkim_exim_verify_feed(uschar * data, int len) +{ +int rc; + +store_pool = POOL_MESSAGE; +if ( dkim_collect_input + && (rc = pdkim_feed(dkim_verify_ctx, data, len)) != PDKIM_OK) + { + dkim_collect_error = pdkim_errstr(rc); + log_write(0, LOG_MAIN, + "DKIM: validation error: %.100s", dkim_collect_error); + dkim_collect_input = 0; + } +store_pool = dkim_verify_oldpool; +} + + +/* Log the result for the given signature */ +static void +dkim_exim_verify_log_sig(pdkim_signature * sig) +{ +gstring * logmsg; +uschar * s; + +if (!sig) return; + +/* Remember the domain for the first pass result */ + +if ( !dkim_verify_overall + && dkim_verify_status + ? Ustrcmp(dkim_verify_status, US"pass") == 0 + : sig->verify_status == PDKIM_VERIFY_PASS + ) + dkim_verify_overall = string_copy(sig->domain); + +/* Rewrite the sig result if the ACL overrode it. This is only +needed because the DMARC code (sigh) peeks at the dkim sigs. +Mark the sig for this having been done. */ + +if ( dkim_verify_status + && ( dkim_verify_status != dkim_exim_expand_query(DKIM_VERIFY_STATUS) + || dkim_verify_reason != dkim_exim_expand_query(DKIM_VERIFY_REASON) + ) ) + { /* overridden by ACL */ + sig->verify_ext_status = -1; + if (Ustrcmp(dkim_verify_status, US"fail") == 0) + sig->verify_status = PDKIM_VERIFY_POLICY | PDKIM_VERIFY_FAIL; + else if (Ustrcmp(dkim_verify_status, US"invalid") == 0) + sig->verify_status = PDKIM_VERIFY_POLICY | PDKIM_VERIFY_INVALID; + else if (Ustrcmp(dkim_verify_status, US"none") == 0) + sig->verify_status = PDKIM_VERIFY_POLICY | PDKIM_VERIFY_NONE; + else if (Ustrcmp(dkim_verify_status, US"pass") == 0) + sig->verify_status = PDKIM_VERIFY_POLICY | PDKIM_VERIFY_PASS; + else + sig->verify_status = -1; + } + +if (!LOGGING(dkim_verbose)) return; + + +logmsg = string_catn(NULL, US"DKIM: ", 6); +if (!(s = sig->domain)) s = US""; +logmsg = string_append(logmsg, 2, "d=", s); +if (!(s = sig->selector)) s = US""; +logmsg = string_append(logmsg, 2, " s=", s); +logmsg = string_fmt_append(logmsg, " c=%s/%s a=%s b=" SIZE_T_FMT, + sig->canon_headers == PDKIM_CANON_SIMPLE ? "simple" : "relaxed", + sig->canon_body == PDKIM_CANON_SIMPLE ? "simple" : "relaxed", + dkim_sig_to_a_tag(sig), + (int)sig->sighash.len > -1 ? sig->sighash.len * 8 : (size_t)0); +if ((s= sig->identity)) logmsg = string_append(logmsg, 2, " i=", s); +if (sig->created > 0) logmsg = string_fmt_append(logmsg, " t=%lu", + sig->created); +if (sig->expires > 0) logmsg = string_fmt_append(logmsg, " x=%lu", + sig->expires); +if (sig->bodylength > -1) logmsg = string_fmt_append(logmsg, " l=%lu", + sig->bodylength); + +if (sig->verify_status & PDKIM_VERIFY_POLICY) + logmsg = string_append(logmsg, 5, + US" [", dkim_verify_status, US" - ", dkim_verify_reason, US"]"); +else + switch (sig->verify_status) + { + case PDKIM_VERIFY_NONE: + logmsg = string_cat(logmsg, US" [not verified]"); + break; + + case PDKIM_VERIFY_INVALID: + logmsg = string_cat(logmsg, US" [invalid - "); + switch (sig->verify_ext_status) + { + case PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE: + logmsg = string_cat(logmsg, + US"public key record (currently?) unavailable]"); + break; + + case PDKIM_VERIFY_INVALID_BUFFER_SIZE: + logmsg = string_cat(logmsg, US"overlong public key record]"); + break; + + case PDKIM_VERIFY_INVALID_PUBKEY_DNSRECORD: + case PDKIM_VERIFY_INVALID_PUBKEY_IMPORT: + logmsg = string_cat(logmsg, US"syntax error in public key record]"); + break; + + case PDKIM_VERIFY_INVALID_SIGNATURE_ERROR: + logmsg = string_cat(logmsg, US"signature tag missing or invalid]"); + break; + + case PDKIM_VERIFY_INVALID_DKIM_VERSION: + logmsg = string_cat(logmsg, US"unsupported DKIM version]"); + break; + + default: + logmsg = string_cat(logmsg, US"unspecified problem]"); + } + break; + + case PDKIM_VERIFY_FAIL: + logmsg = string_cat(logmsg, US" [verification failed - "); + switch (sig->verify_ext_status) + { + case PDKIM_VERIFY_FAIL_BODY: + logmsg = string_cat(logmsg, + US"body hash mismatch (body probably modified in transit)]"); + break; + + case PDKIM_VERIFY_FAIL_MESSAGE: + logmsg = string_cat(logmsg, + US"signature did not verify " + "(headers probably modified in transit)]"); + break; + + case PDKIM_VERIFY_INVALID_PUBKEY_KEYSIZE: + logmsg = string_cat(logmsg, + US"signature invalid (key too short)]"); + break; + + default: + logmsg = string_cat(logmsg, US"unspecified reason]"); + } + break; + + case PDKIM_VERIFY_PASS: + logmsg = string_cat(logmsg, US" [verification succeeded]"); + break; + } + +log_write(0, LOG_MAIN, "%s", string_from_gstring(logmsg)); +return; +} + + +/* Log a line for each signature */ +void +dkim_exim_verify_log_all(void) +{ +for (pdkim_signature * sig = dkim_signatures; sig; sig = sig->next) + dkim_exim_verify_log_sig(sig); +} + + +void +dkim_exim_verify_finish(void) +{ +int rc; +gstring * g = NULL; +const uschar * errstr = NULL; + +store_pool = POOL_MESSAGE; + +/* Delete eventual previous signature chain */ + +dkim_signers = NULL; +dkim_signatures = NULL; + +if (dkim_collect_error) + { + log_write(0, LOG_MAIN, + "DKIM: Error during validation, disabling signature verification: %.100s", + dkim_collect_error); + f.dkim_disable_verify = TRUE; + goto out; + } + +dkim_collect_input = 0; + +/* Finish DKIM operation and fetch link to signatures chain */ + +rc = pdkim_feed_finish(dkim_verify_ctx, (pdkim_signature **)&dkim_signatures, + &errstr); +if (rc != PDKIM_OK && errstr) + log_write(0, LOG_MAIN, "DKIM: validation error: %s", errstr); + +/* Build a colon-separated list of signing domains (and identities, if present) in dkim_signers */ + +for (pdkim_signature * sig = dkim_signatures; sig; sig = sig->next) + { + if (sig->domain) g = string_append_listele(g, ':', sig->domain); + if (sig->identity) g = string_append_listele(g, ':', sig->identity); + } + +if (g) dkim_signers = g->s; + +out: +store_pool = dkim_verify_oldpool; +} + + + +/* Args as per dkim_exim_acl_run() below */ +static int +dkim_acl_call(uschar * id, gstring ** res_ptr, + uschar ** user_msgptr, uschar ** log_msgptr) +{ +int rc; +DEBUG(D_receive) + debug_printf("calling acl_smtp_dkim for dkim_cur_signer='%s'\n", id); + +rc = acl_check(ACL_WHERE_DKIM, NULL, acl_smtp_dkim, user_msgptr, log_msgptr); +dkim_exim_verify_log_sig(dkim_cur_sig); +*res_ptr = string_append_listele(*res_ptr, ':', dkim_verify_status); +return rc; +} + + + +/* For the given identity, run the DKIM ACL once for each matching signature. + +Arguments + id Identity to look for in dkim signatures + res_ptr ptr to growable string-list of status results, + appended to per ACL run + user_msgptr where to put a user error (for SMTP response) + log_msgptr where to put a logging message (not for SMTP response) + +Returns: OK access is granted by an ACCEPT verb + DISCARD access is granted by a DISCARD verb + FAIL access is denied + FAIL_DROP access is denied; drop the connection + DEFER can't tell at the moment + ERROR disaster +*/ + +int +dkim_exim_acl_run(uschar * id, gstring ** res_ptr, + uschar ** user_msgptr, uschar ** log_msgptr) +{ +uschar * cmp_val; +int rc = -1; + +dkim_verify_status = US"none"; +dkim_verify_reason = US""; +dkim_cur_signer = id; + +if (f.dkim_disable_verify || !id || !dkim_verify_ctx) + return OK; + +/* Find signatures to run ACL on */ + +for (pdkim_signature * sig = dkim_signatures; sig; sig = sig->next) + if ( (cmp_val = Ustrchr(id, '@') != NULL ? US sig->identity : US sig->domain) + && strcmpic(cmp_val, id) == 0 + ) + { + /* The "dkim_domain" and "dkim_selector" expansion variables have + related globals, since they are used in the signing code too. + Instead of inventing separate names for verification, we set + them here. This is easy since a domain and selector is guaranteed + to be in a signature. The other dkim_* expansion items are + dynamically fetched from dkim_cur_sig at expansion time (see + dkim_exim_expand_query() below). */ + + dkim_cur_sig = sig; + dkim_signing_domain = US sig->domain; + dkim_signing_selector = US sig->selector; + dkim_key_length = sig->keybits; + + /* These two return static strings, so we can compare the addr + later to see if the ACL overwrote them. Check that when logging */ + + dkim_verify_status = dkim_exim_expand_query(DKIM_VERIFY_STATUS); + dkim_verify_reason = dkim_exim_expand_query(DKIM_VERIFY_REASON); + + if ((rc = dkim_acl_call(id, res_ptr, user_msgptr, log_msgptr)) != OK) + return rc; + } + +if (rc != -1) + return rc; + +/* No matching sig found. Call ACL once anyway. */ + +dkim_cur_sig = NULL; +return dkim_acl_call(id, res_ptr, user_msgptr, log_msgptr); +} + + +static uschar * +dkim_exim_expand_defaults(int what) +{ +switch (what) + { + case DKIM_ALGO: return US""; + case DKIM_BODYLENGTH: return US"9999999999999"; + case DKIM_CANON_BODY: return US""; + case DKIM_CANON_HEADERS: return US""; + case DKIM_COPIEDHEADERS: return US""; + case DKIM_CREATED: return US"0"; + case DKIM_EXPIRES: return US"9999999999999"; + case DKIM_HEADERNAMES: return US""; + case DKIM_IDENTITY: return US""; + case DKIM_KEY_GRANULARITY: return US"*"; + case DKIM_KEY_SRVTYPE: return US"*"; + case DKIM_KEY_NOTES: return US""; + case DKIM_KEY_TESTING: return US"0"; + case DKIM_NOSUBDOMAINS: return US"0"; + case DKIM_VERIFY_STATUS: return US"none"; + case DKIM_VERIFY_REASON: return US""; + default: return US""; + } +} + + +uschar * +dkim_exim_expand_query(int what) +{ +if (!dkim_verify_ctx || f.dkim_disable_verify || !dkim_cur_sig) + return dkim_exim_expand_defaults(what); + +switch (what) + { + case DKIM_ALGO: + return dkim_sig_to_a_tag(dkim_cur_sig); + + case DKIM_BODYLENGTH: + return dkim_cur_sig->bodylength >= 0 + ? string_sprintf("%ld", dkim_cur_sig->bodylength) + : dkim_exim_expand_defaults(what); + + case DKIM_CANON_BODY: + switch (dkim_cur_sig->canon_body) + { + case PDKIM_CANON_RELAXED: return US"relaxed"; + case PDKIM_CANON_SIMPLE: + default: return US"simple"; + } + + case DKIM_CANON_HEADERS: + switch (dkim_cur_sig->canon_headers) + { + case PDKIM_CANON_RELAXED: return US"relaxed"; + case PDKIM_CANON_SIMPLE: + default: return US"simple"; + } + + case DKIM_COPIEDHEADERS: + return dkim_cur_sig->copiedheaders + ? US dkim_cur_sig->copiedheaders : dkim_exim_expand_defaults(what); + + case DKIM_CREATED: + return dkim_cur_sig->created > 0 + ? string_sprintf("%lu", dkim_cur_sig->created) + : dkim_exim_expand_defaults(what); + + case DKIM_EXPIRES: + return dkim_cur_sig->expires > 0 + ? string_sprintf("%lu", dkim_cur_sig->expires) + : dkim_exim_expand_defaults(what); + + case DKIM_HEADERNAMES: + return dkim_cur_sig->headernames + ? dkim_cur_sig->headernames : dkim_exim_expand_defaults(what); + + case DKIM_IDENTITY: + return dkim_cur_sig->identity + ? US dkim_cur_sig->identity : dkim_exim_expand_defaults(what); + + case DKIM_KEY_GRANULARITY: + return dkim_cur_sig->pubkey + ? dkim_cur_sig->pubkey->granularity + ? US dkim_cur_sig->pubkey->granularity + : dkim_exim_expand_defaults(what) + : dkim_exim_expand_defaults(what); + + case DKIM_KEY_SRVTYPE: + return dkim_cur_sig->pubkey + ? dkim_cur_sig->pubkey->srvtype + ? US dkim_cur_sig->pubkey->srvtype + : dkim_exim_expand_defaults(what) + : dkim_exim_expand_defaults(what); + + case DKIM_KEY_NOTES: + return dkim_cur_sig->pubkey + ? dkim_cur_sig->pubkey->notes + ? US dkim_cur_sig->pubkey->notes + : dkim_exim_expand_defaults(what) + : dkim_exim_expand_defaults(what); + + case DKIM_KEY_TESTING: + return dkim_cur_sig->pubkey + ? dkim_cur_sig->pubkey->testing + ? US"1" + : dkim_exim_expand_defaults(what) + : dkim_exim_expand_defaults(what); + + case DKIM_NOSUBDOMAINS: + return dkim_cur_sig->pubkey + ? dkim_cur_sig->pubkey->no_subdomaining + ? US"1" + : dkim_exim_expand_defaults(what) + : dkim_exim_expand_defaults(what); + + case DKIM_VERIFY_STATUS: + switch (dkim_cur_sig->verify_status) + { + case PDKIM_VERIFY_INVALID: return US"invalid"; + case PDKIM_VERIFY_FAIL: return US"fail"; + case PDKIM_VERIFY_PASS: return US"pass"; + case PDKIM_VERIFY_NONE: + default: return US"none"; + } + + case DKIM_VERIFY_REASON: + switch (dkim_cur_sig->verify_ext_status) + { + case PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE: + return US"pubkey_unavailable"; + case PDKIM_VERIFY_INVALID_PUBKEY_DNSRECORD:return US"pubkey_dns_syntax"; + case PDKIM_VERIFY_INVALID_PUBKEY_IMPORT: return US"pubkey_der_syntax"; + case PDKIM_VERIFY_INVALID_PUBKEY_KEYSIZE: return US"pubkey_too_short"; + case PDKIM_VERIFY_FAIL_BODY: return US"bodyhash_mismatch"; + case PDKIM_VERIFY_FAIL_MESSAGE: return US"signature_incorrect"; + } + + default: + return US""; + } +} + + +void +dkim_exim_sign_init(void) +{ +int old_pool = store_pool; + +dkim_exim_init(); +store_pool = POOL_MAIN; +pdkim_init_context(&dkim_sign_ctx, FALSE, &dkim_exim_query_dns_txt); +store_pool = old_pool; +} + + +/* Generate signatures for the given file. +If a prefix is given, prepend it to the file for the calculations. + +Return: + NULL: error; error string written + string: signature header(s), or a zero-length string (not an error) +*/ + +gstring * +dkim_exim_sign(int fd, off_t off, uschar * prefix, + struct ob_dkim * dkim, const uschar ** errstr) +{ +const uschar * dkim_domain = NULL; +int sep = 0; +gstring * seen_doms = NULL; +pdkim_signature * sig; +gstring * sigbuf; +int pdkim_rc; +int sread; +uschar buf[4096]; +int save_errno = 0; +int old_pool = store_pool; +uschar * errwhen; +const uschar * s; + +if (dkim->dot_stuffed) + dkim_sign_ctx.flags |= PDKIM_DOT_TERM; + +store_pool = POOL_MAIN; + +if ((s = dkim->dkim_domain) && !(dkim_domain = expand_cstring(s))) + /* expansion error, do not send message. */ + { errwhen = US"dkim_domain"; goto expand_bad; } + +/* Set $dkim_domain expansion variable to each unique domain in list. */ + +if (dkim_domain) + while ((dkim_signing_domain = string_nextinlist(&dkim_domain, &sep, NULL, 0))) + { + const uschar * dkim_sel; + int sel_sep = 0; + + if (dkim_signing_domain[0] == '\0') + continue; + + /* Only sign once for each domain, no matter how often it + appears in the expanded list. */ + + dkim_signing_domain = string_copylc(dkim_signing_domain); + if (match_isinlist(dkim_signing_domain, CUSS &seen_doms, + 0, NULL, NULL, MCL_STRING, TRUE, NULL) == OK) + continue; + + seen_doms = string_append_listele(seen_doms, ':', dkim_signing_domain); + + /* Set $dkim_selector expansion variable to each selector in list, + for this domain. */ + + if (!(dkim_sel = expand_string(dkim->dkim_selector))) + { errwhen = US"dkim_selector"; goto expand_bad; } + + while ((dkim_signing_selector = string_nextinlist(&dkim_sel, &sel_sep, + NULL, 0))) + { + uschar * dkim_canon_expanded; + int pdkim_canon; + uschar * dkim_sign_headers_expanded = NULL; + uschar * dkim_private_key_expanded; + uschar * dkim_hash_expanded; + uschar * dkim_identity_expanded = NULL; + uschar * dkim_timestamps_expanded = NULL; + unsigned long tval = 0, xval = 0; + + /* Get canonicalization to use */ + + dkim_canon_expanded = dkim->dkim_canon + ? expand_string(dkim->dkim_canon) : US"relaxed"; + if (!dkim_canon_expanded) /* expansion error, do not send message. */ + { errwhen = US"dkim_canon"; goto expand_bad; } + + if (Ustrcmp(dkim_canon_expanded, "relaxed") == 0) + pdkim_canon = PDKIM_CANON_RELAXED; + else if (Ustrcmp(dkim_canon_expanded, "simple") == 0) + pdkim_canon = PDKIM_CANON_SIMPLE; + else + { + log_write(0, LOG_MAIN, + "DKIM: unknown canonicalization method '%s', defaulting to 'relaxed'.\n", + dkim_canon_expanded); + pdkim_canon = PDKIM_CANON_RELAXED; + } + + if ( dkim->dkim_sign_headers + && !(dkim_sign_headers_expanded = expand_string(dkim->dkim_sign_headers))) + { errwhen = US"dkim_sign_header"; goto expand_bad; } + /* else pass NULL, which means default header list */ + + /* Get private key to use. */ + + if (!(dkim_private_key_expanded = expand_string(dkim->dkim_private_key))) + { errwhen = US"dkim_private_key"; goto expand_bad; } + + if ( Ustrlen(dkim_private_key_expanded) == 0 + || Ustrcmp(dkim_private_key_expanded, "0") == 0 + || Ustrcmp(dkim_private_key_expanded, "false") == 0 + ) + continue; /* don't sign, but no error */ + + if ( dkim_private_key_expanded[0] == '/' + && !(dkim_private_key_expanded = + expand_file_big_buffer(dkim_private_key_expanded))) + goto bad; + + if (!(dkim_hash_expanded = expand_string(dkim->dkim_hash))) + { errwhen = US"dkim_hash"; goto expand_bad; } + + if (dkim->dkim_identity) + if (!(dkim_identity_expanded = expand_string(dkim->dkim_identity))) + { errwhen = US"dkim_identity"; goto expand_bad; } + else if (!*dkim_identity_expanded) + dkim_identity_expanded = NULL; + + if (dkim->dkim_timestamps) + if (!(dkim_timestamps_expanded = expand_string(dkim->dkim_timestamps))) + { errwhen = US"dkim_timestamps"; goto expand_bad; } + else + xval = (tval = (unsigned long) time(NULL)) + + strtoul(CCS dkim_timestamps_expanded, NULL, 10); + + if (!(sig = pdkim_init_sign(&dkim_sign_ctx, dkim_signing_domain, + dkim_signing_selector, + dkim_private_key_expanded, + dkim_hash_expanded, + errstr + ))) + goto bad; + dkim_private_key_expanded[0] = '\0'; + + pdkim_set_optional(sig, + CS dkim_sign_headers_expanded, + CS dkim_identity_expanded, + pdkim_canon, + pdkim_canon, -1, tval, xval); + + if (!pdkim_set_sig_bodyhash(&dkim_sign_ctx, sig)) + goto bad; + + if (!dkim_sign_ctx.sig) /* link sig to context chain */ + dkim_sign_ctx.sig = sig; + else + { + pdkim_signature * n = dkim_sign_ctx.sig; + while (n->next) n = n->next; + n->next = sig; + } + } + } + +/* We may need to carry on with the data-feed even if there are no DKIM sigs to +produce, if some other package (eg. ARC) is signing. */ + +if (!dkim_sign_ctx.sig && !dkim->force_bodyhash) + { + DEBUG(D_transport) debug_printf("DKIM: no viable signatures to use\n"); + sigbuf = string_get(1); /* return a zero-len string */ + } +else + { + if (prefix && (pdkim_rc = pdkim_feed(&dkim_sign_ctx, prefix, Ustrlen(prefix))) != PDKIM_OK) + goto pk_bad; + + if (lseek(fd, off, SEEK_SET) < 0) + sread = -1; + else + while ((sread = read(fd, &buf, sizeof(buf))) > 0) + if ((pdkim_rc = pdkim_feed(&dkim_sign_ctx, buf, sread)) != PDKIM_OK) + goto pk_bad; + + /* Handle failed read above. */ + if (sread == -1) + { + debug_printf("DKIM: Error reading -K file.\n"); + save_errno = errno; + goto bad; + } + + /* Build string of headers, one per signature */ + + if ((pdkim_rc = pdkim_feed_finish(&dkim_sign_ctx, &sig, errstr)) != PDKIM_OK) + goto pk_bad; + + if (!sig) + { + DEBUG(D_transport) debug_printf("DKIM: no signatures to use\n"); + sigbuf = string_get(1); /* return a zero-len string */ + } + else for (sigbuf = NULL; sig; sig = sig->next) + sigbuf = string_append(sigbuf, 2, US sig->signature_header, US"\r\n"); + } + +CLEANUP: + (void) string_from_gstring(sigbuf); + store_pool = old_pool; + errno = save_errno; + return sigbuf; + +pk_bad: + log_write(0, LOG_MAIN|LOG_PANIC, + "DKIM: signing failed: %.100s", pdkim_errstr(pdkim_rc)); +bad: + sigbuf = NULL; + goto CLEANUP; + +expand_bad: + *errstr = string_sprintf("failed to expand %s: %s", + errwhen, expand_string_message); + log_write(0, LOG_MAIN | LOG_PANIC, "%s", *errstr); + goto bad; +} + + + + +gstring * +authres_dkim(gstring * g) +{ +int start = 0; /* compiler quietening */ + +DEBUG(D_acl) start = g->ptr; + +for (pdkim_signature * sig = dkim_signatures; sig; sig = sig->next) + { + g = string_catn(g, US";\n\tdkim=", 8); + + if (sig->verify_status & PDKIM_VERIFY_POLICY) + g = string_append(g, 5, + US"policy (", dkim_verify_status, US" - ", dkim_verify_reason, US")"); + else switch(sig->verify_status) + { + case PDKIM_VERIFY_NONE: g = string_cat(g, US"none"); break; + case PDKIM_VERIFY_INVALID: + switch (sig->verify_ext_status) + { + case PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE: + g = string_cat(g, US"tmperror (pubkey unavailable)\n\t\t"); break; + case PDKIM_VERIFY_INVALID_BUFFER_SIZE: + g = string_cat(g, US"permerror (overlong public key record)\n\t\t"); break; + case PDKIM_VERIFY_INVALID_PUBKEY_DNSRECORD: + case PDKIM_VERIFY_INVALID_PUBKEY_IMPORT: + g = string_cat(g, US"neutral (public key record import problem)\n\t\t"); + break; + case PDKIM_VERIFY_INVALID_SIGNATURE_ERROR: + g = string_cat(g, US"neutral (signature tag missing or invalid)\n\t\t"); + break; + case PDKIM_VERIFY_INVALID_DKIM_VERSION: + g = string_cat(g, US"neutral (unsupported DKIM version)\n\t\t"); + break; + default: + g = string_cat(g, US"permerror (unspecified problem)\n\t\t"); break; + } + break; + case PDKIM_VERIFY_FAIL: + switch (sig->verify_ext_status) + { + case PDKIM_VERIFY_FAIL_BODY: + g = string_cat(g, + US"fail (body hash mismatch; body probably modified in transit)\n\t\t"); + break; + case PDKIM_VERIFY_FAIL_MESSAGE: + g = string_cat(g, + US"fail (signature did not verify; headers probably modified in transit)\n\t\t"); + break; + case PDKIM_VERIFY_INVALID_PUBKEY_KEYSIZE: /* should this really be "polcy"? */ + g = string_fmt_append(g, "fail (public key too short: %u bits)\n\t\t", sig->keybits); + break; + default: + g = string_cat(g, US"fail (unspecified reason)\n\t\t"); + break; + } + break; + case PDKIM_VERIFY_PASS: g = string_cat(g, US"pass"); break; + default: g = string_cat(g, US"permerror"); break; + } + if (sig->domain) g = string_append(g, 2, US" header.d=", sig->domain); + if (sig->identity) g = string_append(g, 2, US" header.i=", sig->identity); + if (sig->selector) g = string_append(g, 2, US" header.s=", sig->selector); + g = string_append(g, 2, US" header.a=", dkim_sig_to_a_tag(sig)); + } + +DEBUG(D_acl) + if (g->ptr == start) + debug_printf("DKIM: no authres\n"); + else + debug_printf("DKIM: authres '%.*s'\n", g->ptr - start - 3, g->s + start + 3); +return g; +} + + +# endif /*!MACRO_PREDEF*/ +#endif /*!DISABLE_DKIM*/ diff --git a/src/dkim.h b/src/dkim.h new file mode 100644 index 0000000..7b94f22 --- /dev/null +++ b/src/dkim.h @@ -0,0 +1,32 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge, 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + +void dkim_exim_init(void); +gstring * dkim_exim_sign(int, off_t, uschar *, struct ob_dkim *, const uschar **); +void dkim_exim_verify_init(BOOL); +void dkim_exim_verify_feed(uschar *, int); +void dkim_exim_verify_finish(void); +void dkim_exim_verify_log_all(void); +int dkim_exim_acl_run(uschar *, gstring **, uschar **, uschar **); +uschar *dkim_exim_expand_query(int); + +#define DKIM_ALGO 1 +#define DKIM_BODYLENGTH 2 +#define DKIM_CANON_BODY 3 +#define DKIM_CANON_HEADERS 4 +#define DKIM_COPIEDHEADERS 5 +#define DKIM_CREATED 6 +#define DKIM_EXPIRES 7 +#define DKIM_HEADERNAMES 8 +#define DKIM_IDENTITY 9 +#define DKIM_KEY_GRANULARITY 10 +#define DKIM_KEY_SRVTYPE 11 +#define DKIM_KEY_NOTES 12 +#define DKIM_KEY_TESTING 13 +#define DKIM_NOSUBDOMAINS 14 +#define DKIM_VERIFY_STATUS 15 +#define DKIM_VERIFY_REASON 16 diff --git a/src/dkim_transport.c b/src/dkim_transport.c new file mode 100644 index 0000000..cfd4b90 --- /dev/null +++ b/src/dkim_transport.c @@ -0,0 +1,412 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2022 */ +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Transport shim for dkim signing */ + + +#include "exim.h" + +#ifndef DISABLE_DKIM /* rest of file */ + + +static BOOL +dkt_sign_fail(struct ob_dkim * dkim, int * errp) +{ +if (dkim->dkim_strict) + { + uschar * dkim_strict_result = expand_string(dkim->dkim_strict); + + if (dkim_strict_result) + if ( strcmpic(dkim_strict_result, US"1") == 0 + || strcmpic(dkim_strict_result, US"true") == 0) + { + /* Set errno to something halfway meaningful */ + *errp = EACCES; + log_write(0, LOG_MAIN, "DKIM: message could not be signed," + " and dkim_strict is set. Deferring message delivery."); + return FALSE; + } + } +return TRUE; +} + +/* Send the file at in_fd down the output fd */ + +static BOOL +dkt_send_file(int out_fd, int in_fd, off_t off +#ifdef OS_SENDFILE + , size_t size +#endif + ) +{ +#ifdef OS_SENDFILE +DEBUG(D_transport) debug_printf("send file fd=%d size=%u\n", out_fd, (unsigned)(size - off)); +#else +DEBUG(D_transport) debug_printf("send file fd=%d\n", out_fd); +#endif + +/*XXX should implement timeout, like transport_write_block_fd() ? */ + +#ifdef OS_SENDFILE +/* We can use sendfile() to shove the file contents + to the socket. However only if we don't use TLS, + as then there's another layer of indirection + before the data finally hits the socket. */ +if (tls_out.active.sock != out_fd) + { + ssize_t copied = 0; + + while(copied >= 0 && off < size) + copied = os_sendfile(out_fd, in_fd, &off, size - off); + if (copied < 0) + return FALSE; + } +else + +#endif + + { + int sread, wwritten; + + /* Rewind file */ + if (lseek(in_fd, off, SEEK_SET) < 0) return FALSE; + + /* Send file down the original fd */ + while((sread = read(in_fd, deliver_out_buffer, DELIVER_OUT_BUFFER_SIZE)) > 0) + { + uschar * p = deliver_out_buffer; + /* write the chunk */ + + while (sread) + { +#ifndef DISABLE_TLS + wwritten = tls_out.active.sock == out_fd + ? tls_write(tls_out.active.tls_ctx, p, sread, FALSE) + : write(out_fd, CS p, sread); +#else + wwritten = write(out_fd, CS p, sread); +#endif + if (wwritten == -1) + return FALSE; + p += wwritten; + sread -= wwritten; + } + } + + if (sread == -1) + return FALSE; + } + +return TRUE; +} + + + + +/* This function is a wrapper around transport_write_message(). + It is only called from the smtp transport if DKIM or Domainkeys support + is active and no transport filter is to be used. + +Arguments: + As for transport_write_message() in transort.c, with additional arguments + for DKIM. + +Returns: TRUE on success; FALSE (with errno) for any failure +*/ + +static BOOL +dkt_direct(transport_ctx * tctx, struct ob_dkim * dkim, + const uschar ** err) +{ +int save_fd = tctx->u.fd; +int save_options = tctx->options; +BOOL save_wireformat = f.spool_file_wireformat; +uschar * hdrs; +gstring * dkim_signature; +int hsize; +const uschar * errstr; +BOOL rc; + +DEBUG(D_transport) debug_printf("dkim signing direct-mode\n"); + +/* Get headers in string for signing and transmission. Do CRLF +and dotstuffing (but no body nor dot-termination) */ + +tctx->u.msg = NULL; +tctx->options = tctx->options & ~(topt_end_dot | topt_use_bdat) + | topt_output_string | topt_no_body; + +rc = transport_write_message(tctx, 0); +hdrs = string_from_gstring(tctx->u.msg); +hsize = tctx->u.msg->ptr; + +tctx->u.fd = save_fd; +tctx->options = save_options; +if (!rc) return FALSE; + +/* Get signatures for headers plus spool data file */ + +#ifdef EXPERIMENTAL_ARC +arc_sign_init(); +#endif + +/* The dotstuffed status of the datafile depends on whether it was stored +in wireformat. */ + +dkim->dot_stuffed = f.spool_file_wireformat; +if (!(dkim_signature = dkim_exim_sign(deliver_datafile, SPOOL_DATA_START_OFFSET, + hdrs, dkim, &errstr))) + if (!(rc = dkt_sign_fail(dkim, &errno))) + { + *err = errstr; + return FALSE; + } + +#ifdef EXPERIMENTAL_ARC +if (dkim->arc_signspec) /* Prepend ARC headers */ + { + uschar * e = NULL; + if (!(dkim_signature = arc_sign(dkim->arc_signspec, dkim_signature, &e))) + { + *err = e; + return FALSE; + } + } +#endif + +/* Write the signature and headers into the deliver-out-buffer. This should +mean they go out in the same packet as the MAIL, RCPT and (first) BDAT commands +(transport_write_message() sizes the BDAT for the buffered amount) - for short +messages, the BDAT LAST command. We want no dotstuffing expansion here, it +having already been done - but we have to say we want CRLF output format, and +temporarily set the marker for possible already-CRLF input. */ + +tctx->options &= ~topt_escape_headers; +f.spool_file_wireformat = TRUE; +transport_write_reset(0); +if ( ( dkim_signature + && dkim_signature->ptr > 0 + && !write_chunk(tctx, dkim_signature->s, dkim_signature->ptr) + ) + || !write_chunk(tctx, hdrs, hsize) + ) + return FALSE; + +f.spool_file_wireformat = save_wireformat; +tctx->options = save_options | topt_no_headers | topt_continuation; + +if (!(transport_write_message(tctx, 0))) + return FALSE; + +tctx->options = save_options; +return TRUE; +} + + +/* This function is a wrapper around transport_write_message(). + It is only called from the smtp transport if DKIM or Domainkeys support + is active and a transport filter is to be used. The function sets up a + replacement fd into a -K file, then calls the normal function. This way, the + exact bits that exim would have put "on the wire" will end up in the file + (except for TLS encapsulation, which is the very very last thing). When we + are done signing the file, send the signed message down the original fd (or + TLS fd). + +Arguments: + As for transport_write_message() in transort.c, with additional arguments + for DKIM. + +Returns: TRUE on success; FALSE (with errno) for any failure +*/ + +static BOOL +dkt_via_kfile(transport_ctx * tctx, struct ob_dkim * dkim, const uschar ** err) +{ +int dkim_fd; +int save_errno = 0; +BOOL rc; +uschar * dkim_spool_name; +gstring * dkim_signature; +int options, dlen; +off_t k_file_size; +const uschar * errstr; + +dkim_spool_name = spool_fname(US"input", message_subdir, message_id, + string_sprintf("-%d-K", (int)getpid())); + +DEBUG(D_transport) debug_printf("dkim signing via file %s\n", dkim_spool_name); + +if ((dkim_fd = Uopen(dkim_spool_name, O_RDWR|O_CREAT|O_TRUNC, SPOOL_MODE)) < 0) + { + /* Can't create spool file. Ugh. */ + rc = FALSE; + save_errno = errno; + *err = string_sprintf("dkim spoolfile create: %s", strerror(errno)); + goto CLEANUP; + } + +/* Call transport utility function to write the -K file; does the CRLF expansion +(but, in the CHUNKING case, neither dot-stuffing nor dot-termination). */ + + { + int save_fd = tctx->u.fd; + tctx->u.fd = dkim_fd; + options = tctx->options; + tctx->options &= ~topt_use_bdat; + + rc = transport_write_message(tctx, 0); + + tctx->u.fd = save_fd; + tctx->options = options; + } + +/* Save error state. We must clean up before returning. */ +if (!rc) + { + save_errno = errno; + goto CLEANUP; + } + +#ifdef EXPERIMENTAL_ARC +arc_sign_init(); +#endif + +/* Feed the file to the goats^W DKIM lib. At this point the dotstuffed +status of the file depends on the output of transport_write_message() just +above, which should be the result of the end_dot flag in tctx->options. */ + +dkim->dot_stuffed = !!(options & topt_end_dot); +if (!(dkim_signature = dkim_exim_sign(dkim_fd, 0, NULL, dkim, &errstr))) + { + dlen = 0; + if (!(rc = dkt_sign_fail(dkim, &save_errno))) + { + *err = errstr; + goto CLEANUP; + } + } +else + dlen = dkim_signature->ptr; + +#ifdef EXPERIMENTAL_ARC +if (dkim->arc_signspec) /* Prepend ARC headers */ + { + if (!(dkim_signature = arc_sign(dkim->arc_signspec, dkim_signature, USS err))) + goto CLEANUP; + dlen = dkim_signature->ptr; + } +#endif + +#ifndef OS_SENDFILE +if (options & topt_use_bdat) +#endif + if ((k_file_size = lseek(dkim_fd, 0, SEEK_END)) < 0) + { + *err = string_sprintf("dkim spoolfile seek: %s", strerror(errno)); + goto CLEANUP; + } + +if (options & topt_use_bdat) + { + /* On big messages output a precursor chunk to get any pipelined + MAIL & RCPT commands flushed, then reap the responses so we can + error out on RCPT rejects before sending megabytes. */ + + if ( dlen + k_file_size > DELIVER_OUT_BUFFER_SIZE + && dlen > 0) + { + if ( tctx->chunk_cb(tctx, dlen, 0) != OK + || !transport_write_block(tctx, + dkim_signature->s, dlen, FALSE) + || tctx->chunk_cb(tctx, 0, tc_reap_prev) != OK + ) + goto err; + dlen = 0; + } + + /* Send the BDAT command for the entire message, as a single LAST-marked + chunk. */ + + if (tctx->chunk_cb(tctx, dlen + k_file_size, tc_chunk_last) != OK) + goto err; + } + +if(dlen > 0 && !transport_write_block(tctx, dkim_signature->s, dlen, TRUE)) + goto err; + +if (!dkt_send_file(tctx->u.fd, dkim_fd, 0 +#ifdef OS_SENDFILE + , k_file_size +#endif + )) + { + save_errno = errno; + rc = FALSE; + } + +CLEANUP: + /* unlink -K file */ + if (dkim_fd >= 0) (void)close(dkim_fd); + Uunlink(dkim_spool_name); + errno = save_errno; + return rc; + +err: + save_errno = errno; + rc = FALSE; + goto CLEANUP; +} + + + +/*************************************************************************************************** +* External interface to write the message, while signing it with DKIM and/or Domainkeys * +***************************************************************************************************/ + +/* This function is a wrapper around transport_write_message(). + It is only called from the smtp transport if DKIM or Domainkeys support + is compiled in. + +Arguments: + As for transport_write_message() in transort.c, with additional arguments + for DKIM. + +Returns: TRUE on success; FALSE (with errno) for any failure +*/ + +BOOL +dkim_transport_write_message(transport_ctx * tctx, + struct ob_dkim * dkim, const uschar ** err) +{ +/* If we can't sign, just call the original function. */ + +if ( !(dkim->dkim_private_key && dkim->dkim_domain && dkim->dkim_selector) + && !dkim->force_bodyhash) + return transport_write_message(tctx, 0); + +/* If there is no filter command set up, construct the message and calculate +a dkim signature of it, send the signature and a reconstructed message. This +avoids using a temprary file. */ + +if ( !transport_filter_argv + || !*transport_filter_argv + || !**transport_filter_argv + ) + return dkt_direct(tctx, dkim, err); + +/* Use the transport path to write a file, calculate a dkim signature, +send the signature and then send the file. */ + +return dkt_via_kfile(tctx, dkim, err); +} + +#endif /* whole file */ + +/* vi: aw ai sw=2 +*/ +/* End of dkim_transport.c */ diff --git a/src/dmarc.c b/src/dmarc.c new file mode 100644 index 0000000..17bba9d --- /dev/null +++ b/src/dmarc.c @@ -0,0 +1,655 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ +/* DMARC support. + Copyright (c) The Exim Maintainers 2019 - 2022 + Copyright (c) Todd Lyons 2012 - 2014 + License: GPL */ + +/* Portions Copyright (c) 2012, 2013, The Trusted Domain Project; + All rights reserved, licensed for use per LICENSE.opendmarc. */ + +/* Code for calling dmarc checks via libopendmarc. Called from acl.c. */ + +#include "exim.h" +#ifdef SUPPORT_DMARC +# if !defined SUPPORT_SPF +# error SPF must also be enabled for DMARC +# elif defined DISABLE_DKIM +# error DKIM must also be enabled for DMARC +# else + +# include "functions.h" +# include "dmarc.h" +# include "pdkim/pdkim.h" + +OPENDMARC_LIB_T dmarc_ctx; +DMARC_POLICY_T *dmarc_pctx = NULL; +OPENDMARC_STATUS_T libdm_status, action, dmarc_policy; +OPENDMARC_STATUS_T da, sa, action; +BOOL dmarc_abort = FALSE; +uschar *dmarc_pass_fail = US"skipped"; +header_line *from_header = NULL; +extern SPF_response_t *spf_response; +int dmarc_spf_ares_result = 0; +uschar *spf_sender_domain = NULL; +uschar *spf_human_readable = NULL; +u_char *header_from_sender = NULL; +int history_file_status = DMARC_HIST_OK; +uschar *dkim_history_buffer= NULL; + +typedef struct dmarc_exim_p { + uschar *name; + int value; +} dmarc_exim_p; + +static dmarc_exim_p dmarc_policy_description[] = { + /* name value */ + { US"", DMARC_RECORD_P_UNSPECIFIED }, + { US"none", DMARC_RECORD_P_NONE }, + { US"quarantine", DMARC_RECORD_P_QUARANTINE }, + { US"reject", DMARC_RECORD_P_REJECT }, + { NULL, 0 } +}; + + +gstring * +dmarc_version_report(gstring * g) +{ +return string_fmt_append(g, "Library version: dmarc: Compile: %d.%d.%d.%d\n", + (OPENDMARC_LIB_VERSION & 0xff000000) >> 24, (OPENDMARC_LIB_VERSION & 0x00ff0000) >> 16, + (OPENDMARC_LIB_VERSION & 0x0000ff00) >> 8, OPENDMARC_LIB_VERSION & 0x000000ff); +} + + +/* Accept an error_block struct, initialize if empty, parse to the +end, and append the two strings passed to it. Used for adding +variable amounts of value:pair data to the forensic emails. */ + +static error_block * +add_to_eblock(error_block *eblock, uschar *t1, uschar *t2) +{ +error_block *eb = store_malloc(sizeof(error_block)); +if (!eblock) + eblock = eb; +else + { + /* Find the end of the eblock struct and point it at eb */ + error_block *tmp = eblock; + while(tmp->next) + tmp = tmp->next; + tmp->next = eb; + } +eb->text1 = t1; +eb->text2 = t2; +eb->next = NULL; +return eblock; +} + +/* dmarc_init sets up a context that can be re-used for several +messages on the same SMTP connection (that come from the +same host with the same HELO string) */ + +int +dmarc_init() +{ +int *netmask = NULL; /* Ignored */ +int is_ipv6 = 0; + +/* Set some sane defaults. Also clears previous results when + * multiple messages in one connection. */ +dmarc_pctx = NULL; +dmarc_status = US"none"; +dmarc_abort = FALSE; +dmarc_pass_fail = US"skipped"; +dmarc_used_domain = US""; +f.dmarc_has_been_checked = FALSE; +header_from_sender = NULL; +spf_sender_domain = NULL; +spf_human_readable = NULL; + +/* ACLs have "control=dmarc_disable_verify" */ +if (f.dmarc_disable_verify == TRUE) + return OK; + +(void) memset(&dmarc_ctx, '\0', sizeof dmarc_ctx); +dmarc_ctx.nscount = 0; +libdm_status = opendmarc_policy_library_init(&dmarc_ctx); +if (libdm_status != DMARC_PARSE_OKAY) + { + log_write(0, LOG_MAIN|LOG_PANIC, "DMARC failure to init library: %s", + opendmarc_policy_status_to_str(libdm_status)); + dmarc_abort = TRUE; + } +if (!dmarc_tld_file || !*dmarc_tld_file) + { + DEBUG(D_receive) debug_printf("DMARC: no dmarc_tld_file\n"); + dmarc_abort = TRUE; + } +else if (opendmarc_tld_read_file(CS dmarc_tld_file, NULL, NULL, NULL)) + { + log_write(0, LOG_MAIN|LOG_PANIC, "DMARC failure to load tld list '%s': %s", + dmarc_tld_file, strerror(errno)); + dmarc_abort = TRUE; + } +if (!sender_host_address) + { + DEBUG(D_receive) debug_printf("DMARC: no sender_host_address\n"); + dmarc_abort = TRUE; + } +/* This catches locally originated email and startup errors above. */ +if (!dmarc_abort) + { + is_ipv6 = string_is_ip_address(sender_host_address, netmask) == 6; + if (!(dmarc_pctx = opendmarc_policy_connect_init(sender_host_address, is_ipv6))) + { + log_write(0, LOG_MAIN|LOG_PANIC, + "DMARC failure creating policy context: ip=%s", sender_host_address); + dmarc_abort = TRUE; + } + } + +return OK; +} + + +/* dmarc_store_data stores the header data so that subsequent +dmarc_process can access the data */ + +int +dmarc_store_data(header_line *hdr) +{ +/* No debug output because would change every test debug output */ +if (!f.dmarc_disable_verify) + from_header = hdr; +return OK; +} + + +static void +dmarc_send_forensic_report(u_char **ruf) +{ +uschar *recipient, *save_sender; +BOOL send_status = FALSE; +error_block *eblock = NULL; +FILE *message_file = NULL; + +/* Earlier ACL does not have *required* control=dmarc_enable_forensic */ +if (!f.dmarc_enable_forensic) + return; + +if ( dmarc_policy == DMARC_POLICY_REJECT && action == DMARC_RESULT_REJECT + || dmarc_policy == DMARC_POLICY_QUARANTINE && action == DMARC_RESULT_QUARANTINE + || dmarc_policy == DMARC_POLICY_NONE && action == DMARC_RESULT_REJECT + || dmarc_policy == DMARC_POLICY_NONE && action == DMARC_RESULT_QUARANTINE + ) + if (ruf) + { + eblock = add_to_eblock(eblock, US"Sender Domain", dmarc_used_domain); + eblock = add_to_eblock(eblock, US"Sender IP Address", sender_host_address); + eblock = add_to_eblock(eblock, US"Received Date", tod_stamp(tod_full)); + eblock = add_to_eblock(eblock, US"SPF Alignment", + sa == DMARC_POLICY_SPF_ALIGNMENT_PASS ? US"yes" : US"no"); + eblock = add_to_eblock(eblock, US"DKIM Alignment", + da == DMARC_POLICY_DKIM_ALIGNMENT_PASS ? US"yes" : US"no"); + eblock = add_to_eblock(eblock, US"DMARC Results", dmarc_status_text); + + for (int c = 0; ruf[c]; c++) + { + recipient = string_copylc(ruf[c]); + if (Ustrncmp(recipient, "mailto:",7)) + continue; + /* Move to first character past the colon */ + recipient += 7; + DEBUG(D_receive) + debug_printf("DMARC forensic report to %s%s\n", recipient, + (host_checking || f.running_in_test_harness) ? " (not really)" : ""); + if (host_checking || f.running_in_test_harness) + continue; + + if (!moan_send_message(recipient, ERRMESS_DMARC_FORENSIC, eblock, + header_list, message_file, NULL)) + log_write(0, LOG_MAIN|LOG_PANIC, + "failure to send DMARC forensic report to %s", recipient); + } + } +} + + +/* Look up a DNS dmarc record for the given domain. Return it or NULL */ + +static uschar * +dmarc_dns_lookup(uschar * dom) +{ +dns_answer * dnsa = store_get_dns_answer(); +dns_scan dnss; +int rc = dns_lookup(dnsa, string_sprintf("_dmarc.%s", dom), T_TXT, NULL); + +if (rc == DNS_SUCCEED) + for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr; + rr = dns_next_rr(dnsa, &dnss, RESET_NEXT)) + if (rr->type == T_TXT && rr->size > 3) + { + store_free_dns_answer(dnsa); + return string_copyn_taint(US rr->data, rr->size, GET_TAINTED); + } +store_free_dns_answer(dnsa); +return NULL; +} + + +static int +dmarc_write_history_file() +{ +int history_file_fd; +ssize_t written_len; +int tmp_ans; +u_char **rua; /* aggregate report addressees */ +uschar *history_buffer = NULL; + +if (!dmarc_history_file) + { + DEBUG(D_receive) debug_printf("DMARC history file not set\n"); + return DMARC_HIST_DISABLED; + } +history_file_fd = log_open_as_exim(dmarc_history_file); + +if (history_file_fd < 0) + { + log_write(0, LOG_MAIN|LOG_PANIC, "failure to create DMARC history file: %s", + dmarc_history_file); + return DMARC_HIST_FILE_ERR; + } + +/* Generate the contents of the history file */ +history_buffer = string_sprintf( + "job %s\nreporter %s\nreceived %ld\nipaddr %s\nfrom %s\nmfrom %s\n", + message_id, primary_hostname, time(NULL), sender_host_address, + header_from_sender, expand_string(US"$sender_address_domain")); + +if (spf_response) + history_buffer = string_sprintf("%sspf %d\n", history_buffer, dmarc_spf_ares_result); + /* history_buffer = string_sprintf("%sspf -1\n", history_buffer); */ + +history_buffer = string_sprintf( + "%s%spdomain %s\npolicy %d\n", + history_buffer, dkim_history_buffer, dmarc_used_domain, dmarc_policy); + +if ((rua = opendmarc_policy_fetch_rua(dmarc_pctx, NULL, 0, 1))) + for (tmp_ans = 0; rua[tmp_ans]; tmp_ans++) + history_buffer = string_sprintf("%srua %s\n", history_buffer, rua[tmp_ans]); +else + history_buffer = string_sprintf("%srua -\n", history_buffer); + +opendmarc_policy_fetch_pct(dmarc_pctx, &tmp_ans); +history_buffer = string_sprintf("%spct %d\n", history_buffer, tmp_ans); + +opendmarc_policy_fetch_adkim(dmarc_pctx, &tmp_ans); +history_buffer = string_sprintf("%sadkim %d\n", history_buffer, tmp_ans); + +opendmarc_policy_fetch_aspf(dmarc_pctx, &tmp_ans); +history_buffer = string_sprintf("%saspf %d\n", history_buffer, tmp_ans); + +opendmarc_policy_fetch_p(dmarc_pctx, &tmp_ans); +history_buffer = string_sprintf("%sp %d\n", history_buffer, tmp_ans); + +opendmarc_policy_fetch_sp(dmarc_pctx, &tmp_ans); +history_buffer = string_sprintf("%ssp %d\n", history_buffer, tmp_ans); + +history_buffer = string_sprintf( + "%salign_dkim %d\nalign_spf %d\naction %d\n", + history_buffer, da, sa, action); + +/* Write the contents to the history file */ +DEBUG(D_receive) + debug_printf("DMARC logging history data for opendmarc reporting%s\n", + (host_checking || f.running_in_test_harness) ? " (not really)" : ""); +if (host_checking || f.running_in_test_harness) + { + DEBUG(D_receive) + debug_printf("DMARC history data for debugging:\n%s", history_buffer); + } +else + { + written_len = write_to_fd_buf(history_file_fd, + history_buffer, + Ustrlen(history_buffer)); + if (written_len == 0) + { + log_write(0, LOG_MAIN|LOG_PANIC, "failure to write to DMARC history file: %s", + dmarc_history_file); + return DMARC_HIST_WRITE_ERR; + } + (void)close(history_file_fd); + } +return DMARC_HIST_OK; +} + + +/* dmarc_process adds the envelope sender address to the existing +context (if any), retrieves the result, sets up expansion +strings and evaluates the condition outcome. */ + +int +dmarc_process() +{ +int sr, origin; /* used in SPF section */ +int dmarc_spf_result = 0; /* stores spf into dmarc conn ctx */ +int tmp_ans, c; +pdkim_signature * sig = dkim_signatures; +uschar * rr; +BOOL has_dmarc_record = TRUE; +u_char **ruf; /* forensic report addressees, if called for */ + +/* ACLs have "control=dmarc_disable_verify" */ +if (f.dmarc_disable_verify) + return OK; + +/* Store the header From: sender domain for this part of DMARC. + * If there is no from_header struct, then it's likely this message + * is locally generated and relying on fixups to add it. Just skip + * the entire DMARC system if we can't find a From: header....or if + * there was a previous error. + */ +if (!from_header) + { + DEBUG(D_receive) debug_printf("DMARC: no From: header\n"); + dmarc_abort = TRUE; + } +else if (!dmarc_abort) + { + uschar * errormsg; + int dummy, domain; + uschar * p; + uschar saveend; + + f.parse_allow_group = TRUE; + p = parse_find_address_end(from_header->text, FALSE); + saveend = *p; *p = '\0'; + if ((header_from_sender = parse_extract_address(from_header->text, &errormsg, + &dummy, &dummy, &domain, FALSE))) + header_from_sender += domain; + *p = saveend; + + /* The opendmarc library extracts the domain from the email address, but + * only try to store it if it's not empty. Otherwise, skip out of DMARC. */ + if (!header_from_sender || (strcmp( CCS header_from_sender, "") == 0)) + dmarc_abort = TRUE; + libdm_status = dmarc_abort + ? DMARC_PARSE_OKAY + : opendmarc_policy_store_from_domain(dmarc_pctx, header_from_sender); + if (libdm_status != DMARC_PARSE_OKAY) + { + log_write(0, LOG_MAIN|LOG_PANIC, + "failure to store header From: in DMARC: %s, header was '%s'", + opendmarc_policy_status_to_str(libdm_status), from_header->text); + dmarc_abort = TRUE; + } + } + +/* Skip DMARC if connection is SMTP Auth. Temporarily, admin should + * instead do this in the ACLs. */ +if (!dmarc_abort && !sender_host_authenticated) + { + uschar * dmarc_domain; + + /* Use the envelope sender domain for this part of DMARC */ + spf_sender_domain = expand_string(US"$sender_address_domain"); + if (!spf_response) + { + /* No spf data means null envelope sender so generate a domain name + * from the sender_helo_name */ + if (!spf_sender_domain) + { + spf_sender_domain = sender_helo_name; + log_write(0, LOG_MAIN, "DMARC using synthesized SPF sender domain = %s\n", + spf_sender_domain); + DEBUG(D_receive) + debug_printf("DMARC using synthesized SPF sender domain = %s\n", + spf_sender_domain); + } + dmarc_spf_result = DMARC_POLICY_SPF_OUTCOME_NONE; + dmarc_spf_ares_result = ARES_RESULT_UNKNOWN; + origin = DMARC_POLICY_SPF_ORIGIN_HELO; + spf_human_readable = US""; + } + else + { + sr = spf_response->result; + dmarc_spf_result = sr == SPF_RESULT_NEUTRAL ? DMARC_POLICY_SPF_OUTCOME_NONE : + sr == SPF_RESULT_PASS ? DMARC_POLICY_SPF_OUTCOME_PASS : + sr == SPF_RESULT_FAIL ? DMARC_POLICY_SPF_OUTCOME_FAIL : + sr == SPF_RESULT_SOFTFAIL ? DMARC_POLICY_SPF_OUTCOME_TMPFAIL : + DMARC_POLICY_SPF_OUTCOME_NONE; + dmarc_spf_ares_result = sr == SPF_RESULT_NEUTRAL ? ARES_RESULT_NEUTRAL : + sr == SPF_RESULT_PASS ? ARES_RESULT_PASS : + sr == SPF_RESULT_FAIL ? ARES_RESULT_FAIL : + sr == SPF_RESULT_SOFTFAIL ? ARES_RESULT_SOFTFAIL : + sr == SPF_RESULT_NONE ? ARES_RESULT_NONE : + sr == SPF_RESULT_TEMPERROR ? ARES_RESULT_TEMPERROR : + sr == SPF_RESULT_PERMERROR ? ARES_RESULT_PERMERROR : + ARES_RESULT_UNKNOWN; + origin = DMARC_POLICY_SPF_ORIGIN_MAILFROM; + spf_human_readable = US spf_response->header_comment; + DEBUG(D_receive) + debug_printf("DMARC using SPF sender domain = %s\n", spf_sender_domain); + } + if (strcmp( CCS spf_sender_domain, "") == 0) + dmarc_abort = TRUE; + if (!dmarc_abort) + { + libdm_status = opendmarc_policy_store_spf(dmarc_pctx, spf_sender_domain, + dmarc_spf_result, origin, spf_human_readable); + if (libdm_status != DMARC_PARSE_OKAY) + log_write(0, LOG_MAIN|LOG_PANIC, "failure to store spf for DMARC: %s", + opendmarc_policy_status_to_str(libdm_status)); + } + + /* Now we cycle through the dkim signature results and put into + * the opendmarc context, further building the DMARC reply. */ + dkim_history_buffer = US""; + while (sig) + { + int dkim_result, dkim_ares_result, vs, ves; + + vs = sig->verify_status & ~PDKIM_VERIFY_POLICY; + ves = sig->verify_ext_status; + dkim_result = vs == PDKIM_VERIFY_PASS ? DMARC_POLICY_DKIM_OUTCOME_PASS : + vs == PDKIM_VERIFY_FAIL ? DMARC_POLICY_DKIM_OUTCOME_FAIL : + vs == PDKIM_VERIFY_INVALID ? DMARC_POLICY_DKIM_OUTCOME_TMPFAIL : + DMARC_POLICY_DKIM_OUTCOME_NONE; + libdm_status = opendmarc_policy_store_dkim(dmarc_pctx, US sig->domain, + dkim_result, US""); + DEBUG(D_receive) + debug_printf("DMARC adding DKIM sender domain = %s\n", sig->domain); + if (libdm_status != DMARC_PARSE_OKAY) + log_write(0, LOG_MAIN|LOG_PANIC, + "failure to store dkim (%s) for DMARC: %s", + sig->domain, opendmarc_policy_status_to_str(libdm_status)); + + dkim_ares_result = + vs == PDKIM_VERIFY_PASS ? ARES_RESULT_PASS : + vs == PDKIM_VERIFY_FAIL ? ARES_RESULT_FAIL : + vs == PDKIM_VERIFY_NONE ? ARES_RESULT_NONE : + vs == PDKIM_VERIFY_INVALID ? + ves == PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE ? ARES_RESULT_PERMERROR : + ves == PDKIM_VERIFY_INVALID_BUFFER_SIZE ? ARES_RESULT_PERMERROR : + ves == PDKIM_VERIFY_INVALID_PUBKEY_DNSRECORD ? ARES_RESULT_PERMERROR : + ves == PDKIM_VERIFY_INVALID_PUBKEY_IMPORT ? ARES_RESULT_PERMERROR : + ARES_RESULT_UNKNOWN : + ARES_RESULT_UNKNOWN; + dkim_history_buffer = string_sprintf("%sdkim %s %d\n", dkim_history_buffer, + sig->domain, dkim_ares_result); + sig = sig->next; + } + + /* Look up DMARC policy record in DNS. We do this explicitly, rather than + letting the dmarc library do it with opendmarc_policy_query_dmarc(), so that + our dns access path is used for debug tracing and for the testsuite + diversion. */ + + libdm_status = (rr = dmarc_dns_lookup(header_from_sender)) + ? opendmarc_policy_store_dmarc(dmarc_pctx, rr, header_from_sender, NULL) + : DMARC_DNS_ERROR_NO_RECORD; + switch (libdm_status) + { + case DMARC_DNS_ERROR_NXDOMAIN: + case DMARC_DNS_ERROR_NO_RECORD: + DEBUG(D_receive) + debug_printf("DMARC no record found for %s\n", header_from_sender); + has_dmarc_record = FALSE; + break; + case DMARC_PARSE_OKAY: + DEBUG(D_receive) + debug_printf("DMARC record found for %s\n", header_from_sender); + break; + case DMARC_PARSE_ERROR_BAD_VALUE: + DEBUG(D_receive) + debug_printf("DMARC record parse error for %s\n", header_from_sender); + has_dmarc_record = FALSE; + break; + default: + /* everything else, skip dmarc */ + DEBUG(D_receive) + debug_printf("DMARC skipping (%d), unsure what to do with %s", + libdm_status, from_header->text); + has_dmarc_record = FALSE; + break; + } + +/* Store the policy string in an expandable variable. */ + + libdm_status = opendmarc_policy_fetch_p(dmarc_pctx, &tmp_ans); + for (c = 0; dmarc_policy_description[c].name; c++) + if (tmp_ans == dmarc_policy_description[c].value) + { + dmarc_domain_policy = string_sprintf("%s",dmarc_policy_description[c].name); + break; + } + + /* Can't use exim's string manipulation functions so allocate memory + for libopendmarc using its max hostname length definition. */ + + dmarc_domain = store_get(DMARC_MAXHOSTNAMELEN, GET_TAINTED); + libdm_status = opendmarc_policy_fetch_utilized_domain(dmarc_pctx, + dmarc_domain, DMARC_MAXHOSTNAMELEN-1); + store_release_above(dmarc_domain + Ustrlen(dmarc_domain)+1); + dmarc_used_domain = dmarc_domain; + + if (libdm_status != DMARC_PARSE_OKAY) + log_write(0, LOG_MAIN|LOG_PANIC, + "failure to read domainname used for DMARC lookup: %s", + opendmarc_policy_status_to_str(libdm_status)); + + dmarc_policy = libdm_status = opendmarc_get_policy_to_enforce(dmarc_pctx); + switch(libdm_status) + { + case DMARC_POLICY_ABSENT: /* No DMARC record found */ + dmarc_status = US"norecord"; + dmarc_pass_fail = US"none"; + dmarc_status_text = US"No DMARC record"; + action = DMARC_RESULT_ACCEPT; + break; + case DMARC_FROM_DOMAIN_ABSENT: /* No From: domain */ + dmarc_status = US"nofrom"; + dmarc_pass_fail = US"temperror"; + dmarc_status_text = US"No From: domain found"; + action = DMARC_RESULT_ACCEPT; + break; + case DMARC_POLICY_NONE: /* Accept and report */ + dmarc_status = US"none"; + dmarc_pass_fail = US"none"; + dmarc_status_text = US"None, Accept"; + action = DMARC_RESULT_ACCEPT; + break; + case DMARC_POLICY_PASS: /* Explicit accept */ + dmarc_status = US"accept"; + dmarc_pass_fail = US"pass"; + dmarc_status_text = US"Accept"; + action = DMARC_RESULT_ACCEPT; + break; + case DMARC_POLICY_REJECT: /* Explicit reject */ + dmarc_status = US"reject"; + dmarc_pass_fail = US"fail"; + dmarc_status_text = US"Reject"; + action = DMARC_RESULT_REJECT; + break; + case DMARC_POLICY_QUARANTINE: /* Explicit quarantine */ + dmarc_status = US"quarantine"; + dmarc_pass_fail = US"fail"; + dmarc_status_text = US"Quarantine"; + action = DMARC_RESULT_QUARANTINE; + break; + default: + dmarc_status = US"temperror"; + dmarc_pass_fail = US"temperror"; + dmarc_status_text = US"Internal Policy Error"; + action = DMARC_RESULT_TEMPFAIL; + break; + } + + libdm_status = opendmarc_policy_fetch_alignment(dmarc_pctx, &da, &sa); + if (libdm_status != DMARC_PARSE_OKAY) + log_write(0, LOG_MAIN|LOG_PANIC, "failure to read DMARC alignment: %s", + opendmarc_policy_status_to_str(libdm_status)); + + if (has_dmarc_record) + { + log_write(0, LOG_MAIN, "DMARC results: spf_domain=%s dmarc_domain=%s " + "spf_align=%s dkim_align=%s enforcement='%s'", + spf_sender_domain, dmarc_used_domain, + sa==DMARC_POLICY_SPF_ALIGNMENT_PASS ?"yes":"no", + da==DMARC_POLICY_DKIM_ALIGNMENT_PASS ?"yes":"no", + dmarc_status_text); + history_file_status = dmarc_write_history_file(); + /* Now get the forensic reporting addresses, if any */ + ruf = opendmarc_policy_fetch_ruf(dmarc_pctx, NULL, 0, 1); + dmarc_send_forensic_report(ruf); + } + } + +/* shut down libopendmarc */ +if (dmarc_pctx) + (void) opendmarc_policy_connect_shutdown(dmarc_pctx); +if (!f.dmarc_disable_verify) + (void) opendmarc_policy_library_shutdown(&dmarc_ctx); + +return OK; +} + +uschar * +dmarc_exim_expand_query(int what) +{ +if (f.dmarc_disable_verify || !dmarc_pctx) + return dmarc_exim_expand_defaults(what); + +if (what == DMARC_VERIFY_STATUS) + return dmarc_status; +return US""; +} + +uschar * +dmarc_exim_expand_defaults(int what) +{ +if (what == DMARC_VERIFY_STATUS) + return f.dmarc_disable_verify ? US"off" : US"none"; +return US""; +} + + +gstring * +authres_dmarc(gstring * g) +{ +if (f.dmarc_has_been_checked) + { + g = string_append(g, 2, US";\n\tdmarc=", dmarc_pass_fail); + if (header_from_sender) + g = string_append(g, 2, US" header.from=", header_from_sender); + } +return g; +} + +# endif /* SUPPORT_SPF */ +#endif /* SUPPORT_DMARC */ +/* vi: aw ai sw=2 + */ diff --git a/src/dmarc.h b/src/dmarc.h new file mode 100644 index 0000000..899cd7e --- /dev/null +++ b/src/dmarc.h @@ -0,0 +1,60 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Experimental DMARC support. + Copyright (c) The Exim Maintainers 2021 - 2022 + Copyright (c) Todd Lyons 2012 - 2014 + License: GPL */ + +/* Portions Copyright (c) 2012, 2013, The Trusted Domain Project; + All rights reserved, licensed for use per LICENSE.opendmarc. */ + +#ifdef SUPPORT_DMARC + +# include "opendmarc/dmarc.h" +# ifdef SUPPORT_SPF +# include "spf2/spf.h" +# endif /* SUPPORT_SPF */ + +/* prototypes */ +gstring * dmarc_version_report(gstring *); +int dmarc_init(); +int dmarc_store_data(header_line *); +int dmarc_process(); +uschar *dmarc_exim_expand_query(int); +uschar *dmarc_exim_expand_defaults(int); + +#define DMARC_VERIFY_STATUS 1 + +#define DMARC_HIST_OK 1 +#define DMARC_HIST_DISABLED 2 +#define DMARC_HIST_EMPTY 3 +#define DMARC_HIST_FILE_ERR 4 +#define DMARC_HIST_WRITE_ERR 5 + +/* From opendmarc.c */ +#define DMARC_RESULT_REJECT 0 +#define DMARC_RESULT_DISCARD 1 +#define DMARC_RESULT_ACCEPT 2 +#define DMARC_RESULT_TEMPFAIL 3 +#define DMARC_RESULT_QUARANTINE 4 + +/* From opendmarc-ar.h */ +/* ARES_RESULT_T -- type for specifying an authentication result */ +#define ARES_RESULT_UNDEFINED (-1) +#define ARES_RESULT_PASS 0 +#define ARES_RESULT_UNUSED 1 +#define ARES_RESULT_SOFTFAIL 2 +#define ARES_RESULT_NEUTRAL 3 +#define ARES_RESULT_TEMPERROR 4 +#define ARES_RESULT_PERMERROR 5 +#define ARES_RESULT_NONE 6 +#define ARES_RESULT_FAIL 7 +#define ARES_RESULT_POLICY 8 +#define ARES_RESULT_NXDOMAIN 9 +#define ARES_RESULT_SIGNED 10 +#define ARES_RESULT_UNKNOWN 11 +#define ARES_RESULT_DISCARD 12 + +#endif /* SUPPORT_DMARC */ diff --git a/src/dns.c b/src/dns.c new file mode 100644 index 0000000..7d7ee0c --- /dev/null +++ b/src/dns.c @@ -0,0 +1,1332 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Functions for interfacing with the DNS. */ + +#include "exim.h" + + +/************************************************* +* Fake DNS resolver * +*************************************************/ + +/* This function is called instead of res_search() when Exim is running in its +test harness. It recognizes some special domain names, and uses them to force +failure and retry responses (optionally with a delay). Otherwise, it calls an +external utility that mocks-up a nameserver, if it can find the utility. +If not, it passes its arguments on to res_search(). The fake nameserver may +also return a code specifying that the name should be passed on. + +Background: the original test suite required a real nameserver to carry the +test zones, whereas the new test suite has the fake server for portability. This +code supports both. + +Arguments: + domain the domain name + type the DNS record type + answerptr where to put the answer + size size of the answer area + +Returns: length of returned data, or -1 on error (h_errno set) +*/ + +static int +fakens_search(const uschar *domain, int type, uschar *answerptr, int size) +{ +int len = Ustrlen(domain); +int asize = size; /* Locally modified */ +uschar * name; +uschar utilname[256]; +uschar *aptr = answerptr; /* Locally modified */ +struct stat statbuf; + +/* Remove terminating dot. */ + +if (domain[len - 1] == '.') len--; +name = string_copyn(domain, len); + +/* Look for the fakens utility, and if it exists, call it. */ + +(void)string_format(utilname, sizeof(utilname), "%s/bin/fakens", + config_main_directory); + +if (stat(CS utilname, &statbuf) >= 0) + { + pid_t pid; + int infd, outfd, rc; + uschar *argv[5]; + + DEBUG(D_dns) debug_printf("DNS lookup of %s (%s) using fakens\n", + name, dns_text_type(type)); + + argv[0] = utilname; + argv[1] = config_main_directory; + argv[2] = name; + argv[3] = dns_text_type(type); + argv[4] = NULL; + + pid = child_open(argv, NULL, 0000, &infd, &outfd, FALSE, US"fakens-search"); + if (pid < 0) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "failed to run fakens: %s", + strerror(errno)); + + len = 0; + rc = -1; + while (asize > 0 && (rc = read(outfd, aptr, asize)) > 0) + { + len += rc; + aptr += rc; /* Don't modify the actual arguments, because they */ + asize -= rc; /* may need to be passed on to res_search(). */ + } + + /* If we ran out of output buffer before exhausting the return, + carry on reading and counting it. */ + + if (asize == 0) + while ((rc = read(outfd, name, sizeof(name))) > 0) + len += rc; + + if (rc < 0) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "read from fakens failed: %s", + strerror(errno)); + + switch(child_close(pid, 0)) + { + case 0: return len; + case 1: h_errno = HOST_NOT_FOUND; return -1; + case 2: h_errno = TRY_AGAIN; return -1; + default: + case 3: h_errno = NO_RECOVERY; return -1; + case 4: h_errno = NO_DATA; return -1; + case 5: /* Pass on to res_search() */ + DEBUG(D_dns) debug_printf("fakens returned PASS_ON\n"); + } + } +else + { + DEBUG(D_dns) debug_printf("fakens (%s) not found\n", utilname); + } + +/* fakens utility not found, or it returned "pass on" */ + +DEBUG(D_dns) debug_printf("passing %s on to res_search()\n", domain); + +return res_search(CS domain, C_IN, type, answerptr, size); +} + + + +/************************************************* +* Initialize and configure resolver * +*************************************************/ + +/* Initialize the resolver and the storage for holding DNS answers if this is +the first time we have been here, and set the resolver options. + +Arguments: + qualify_single TRUE to set the RES_DEFNAMES option + search_parents TRUE to set the RES_DNSRCH option + use_dnssec TRUE to set the RES_USE_DNSSEC option + +Returns: nothing +*/ + +void +dns_init(BOOL qualify_single, BOOL search_parents, BOOL use_dnssec) +{ +res_state resp = os_get_dns_resolver_res(); + +if ((resp->options & RES_INIT) == 0) + { + DEBUG(D_resolver) resp->options |= RES_DEBUG; /* For Cygwin */ + os_put_dns_resolver_res(resp); + res_init(); + DEBUG(D_resolver) resp->options |= RES_DEBUG; + os_put_dns_resolver_res(resp); + } + +resp->options &= ~(RES_DNSRCH | RES_DEFNAMES); +resp->options |= (qualify_single? RES_DEFNAMES : 0) | + (search_parents? RES_DNSRCH : 0); +if (dns_retrans > 0) resp->retrans = dns_retrans; +if (dns_retry > 0) resp->retry = dns_retry; + +#ifdef RES_USE_EDNS0 +if (dns_use_edns0 >= 0) + { + if (dns_use_edns0) + resp->options |= RES_USE_EDNS0; + else + resp->options &= ~RES_USE_EDNS0; + DEBUG(D_resolver) + debug_printf("Coerced resolver EDNS0 support %s.\n", + dns_use_edns0 ? "on" : "off"); + } +#else +if (dns_use_edns0 >= 0) + DEBUG(D_resolver) + debug_printf("Unable to %sset EDNS0 without resolver support.\n", + dns_use_edns0 ? "" : "un"); +#endif + +#ifndef DISABLE_DNSSEC +# ifdef RES_USE_DNSSEC +# ifndef RES_USE_EDNS0 +# error Have RES_USE_DNSSEC but not RES_USE_EDNS0? Something hinky ... +# endif +if (use_dnssec) + resp->options |= RES_USE_DNSSEC; +if (dns_dnssec_ok >= 0) + { + if (dns_use_edns0 == 0 && dns_dnssec_ok != 0) + { + DEBUG(D_resolver) + debug_printf("CONFLICT: dns_use_edns0 forced false, dns_dnssec_ok forced true, ignoring latter!\n"); + } + else + { + if (dns_dnssec_ok) + resp->options |= RES_USE_DNSSEC; + else + resp->options &= ~RES_USE_DNSSEC; + DEBUG(D_resolver) debug_printf("Coerced resolver DNSSEC support %s.\n", + dns_dnssec_ok ? "on" : "off"); + } + } +# else +if (dns_dnssec_ok >= 0) + DEBUG(D_resolver) + debug_printf("Unable to %sset DNSSEC without resolver support.\n", + dns_dnssec_ok ? "" : "un"); +if (use_dnssec) + DEBUG(D_resolver) + debug_printf("Unable to set DNSSEC without resolver support.\n"); +# endif +#endif /* DISABLE_DNSSEC */ + +os_put_dns_resolver_res(resp); +} + + + +/************************************************* +* Build key name for PTR records * +*************************************************/ + +/* This function inverts an IP address and adds the relevant domain, to produce +a name that can be used to look up PTR records. + +Arguments: + string the IP address as a string + +Returns: an allocated string +*/ + +uschar * +dns_build_reverse(const uschar * string) +{ +const uschar * p = string + Ustrlen(string); +gstring * g = NULL; + +/* Handle IPv4 address */ + +#if HAVE_IPV6 +if (Ustrchr(string, ':') == NULL) +#endif + { + for (int i = 0; i < 4; i++) + { + const uschar * ppp = p; + while (ppp > string && ppp[-1] != '.') ppp--; + g = string_catn(g, ppp, p - ppp); + g = string_catn(g, US".", 1); + p = ppp - 1; + } + g = string_catn(g, US"in-addr.arpa", 12); + } + +/* Handle IPv6 address; convert to binary so as to fill out any +abbreviation in the textual form. */ + +#if HAVE_IPV6 +else + { + int v6[4]; + + g = string_get_tainted(32, string); + (void)host_aton(string, v6); + + /* The original specification for IPv6 reverse lookup was to invert each + nibble, and look in the ip6.int domain. The domain was subsequently + changed to ip6.arpa. */ + + for (int i = 3; i >= 0; i--) + for (int j = 0; j < 32; j += 4) + g = string_fmt_append(g, "%x.", (v6[i] >> j) & 15); + g = string_catn(g, US"ip6.arpa.", 9); + + /* Another way of doing IPv6 reverse lookups was proposed in conjunction + with A6 records. However, it fell out of favour when they did. The + alternative was to construct a binary key, and look in ip6.arpa. I tried + to make this code do that, but I could not make it work on Solaris 8. The + resolver seems to lose the initial backslash somehow. However, now that + this style of reverse lookup has been dropped, it doesn't matter. These + lines are left here purely for historical interest. */ + + /************************************************** + Ustrcpy(pp, "\\[x"); + pp += 3; + + for (int i = 0; i < 4; i++) + { + sprintf(pp, "%08X", v6[i]); + pp += 8; + } + Ustrcpy(pp, US"].ip6.arpa."); + **************************************************/ + + } +#endif +return string_from_gstring(g); +} + + + + +/* Increment the aptr in dnss, checking against dnsa length. +Return: TRUE for a bad result +*/ +static BOOL +dnss_inc_aptr(const dns_answer * dnsa, dns_scan * dnss, unsigned delta) +{ +return (dnss->aptr += delta) >= dnsa->answer + dnsa->answerlen; +} + +/************************************************* +* Get next DNS record from answer block * +*************************************************/ + +/* Call this with reset == RESET_ANSWERS to scan the answer block, reset == +RESET_AUTHORITY to scan the authority records, reset == RESET_ADDITIONAL to +scan the additional records, and reset == RESET_NEXT to get the next record. +The result is in static storage which must be copied if it is to be preserved. + +Arguments: + dnsa pointer to dns answer block + dnss pointer to dns scan block + reset option specifying what portion to scan, as described above + +Returns: next dns record, or NULL when no more +*/ + +dns_record * +dns_next_rr(const dns_answer *dnsa, dns_scan *dnss, int reset) +{ +const HEADER * h = (const HEADER *)dnsa->answer; +int namelen; + +char * trace = NULL; +#ifdef rr_trace +# define TRACE DEBUG(D_dns) +#else +# define TRACE if (FALSE) +#endif + +/* Reset the saved data when requested to, and skip to the first required RR */ + +if (reset != RESET_NEXT) + { + dnss->rrcount = ntohs(h->qdcount); + TRACE debug_printf("%s: reset (Q rrcount %d)\n", __FUNCTION__, dnss->rrcount); + dnss->aptr = dnsa->answer + sizeof(HEADER); + + /* Skip over questions; failure to expand the name just gives up */ + + while (dnss->rrcount-- > 0) + { + TRACE trace = "Q-namelen"; + namelen = dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen, + dnss->aptr, (DN_EXPAND_ARG4_TYPE) &dnss->srr.name, DNS_MAXNAME); + if (namelen < 0) goto null_return; + /* skip name & type & class */ + TRACE trace = "Q-skip"; + if (dnss_inc_aptr(dnsa, dnss, namelen+4)) goto null_return; + } + + /* Get the number of answer records. */ + + dnss->rrcount = ntohs(h->ancount); + TRACE debug_printf("%s: reset (A rrcount %d)\n", __FUNCTION__, dnss->rrcount); + + /* Skip over answers if we want to look at the authority section. Also skip + the NS records (i.e. authority section) if wanting to look at the additional + records. */ + + if (reset == RESET_ADDITIONAL) + { + TRACE debug_printf("%s: additional\n", __FUNCTION__); + dnss->rrcount += ntohs(h->nscount); + TRACE debug_printf("%s: reset (NS rrcount %d)\n", __FUNCTION__, dnss->rrcount); + } + + if (reset == RESET_AUTHORITY || reset == RESET_ADDITIONAL) + { + TRACE if (reset == RESET_AUTHORITY) + debug_printf("%s: authority\n", __FUNCTION__); + while (dnss->rrcount-- > 0) + { + TRACE trace = "A-namelen"; + namelen = dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen, + dnss->aptr, (DN_EXPAND_ARG4_TYPE) &dnss->srr.name, DNS_MAXNAME); + if (namelen < 0) goto null_return; + /* skip name, type, class & TTL */ + TRACE trace = "A-hdr"; + if (dnss_inc_aptr(dnsa, dnss, namelen+8)) goto null_return; + GETSHORT(dnss->srr.size, dnss->aptr); /* size of data portion */ + /* skip over it */ + TRACE trace = "A-skip"; + if (dnss_inc_aptr(dnsa, dnss, dnss->srr.size)) goto null_return; + } + dnss->rrcount = reset == RESET_AUTHORITY + ? ntohs(h->nscount) : ntohs(h->arcount); + TRACE debug_printf("%s: reset (%s rrcount %d)\n", __FUNCTION__, + reset == RESET_AUTHORITY ? "NS" : "AR", dnss->rrcount); + } + TRACE debug_printf("%s: %d RRs to read\n", __FUNCTION__, dnss->rrcount); + } +else + TRACE debug_printf("%s: next (%d left)\n", __FUNCTION__, dnss->rrcount); + +/* The variable dnss->aptr is now pointing at the next RR, and dnss->rrcount +contains the number of RR records left. */ + +if (dnss->rrcount-- <= 0) return NULL; + +/* If expanding the RR domain name fails, behave as if no more records +(something safe). */ + +TRACE trace = "R-namelen"; +namelen = dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen, dnss->aptr, + (DN_EXPAND_ARG4_TYPE) &dnss->srr.name, DNS_MAXNAME); +if (namelen < 0) goto null_return; + +/* Move the pointer past the name and fill in the rest of the data structure +from the following bytes. */ + +TRACE trace = "R-name"; +if (dnss_inc_aptr(dnsa, dnss, namelen)) goto null_return; + +GETSHORT(dnss->srr.type, dnss->aptr); /* Record type */ +TRACE trace = "R-class"; +if (dnss_inc_aptr(dnsa, dnss, 2)) goto null_return; /* Don't want class */ +GETLONG(dnss->srr.ttl, dnss->aptr); /* TTL */ +GETSHORT(dnss->srr.size, dnss->aptr); /* Size of data portion */ +dnss->srr.data = dnss->aptr; /* The record's data follows */ + +/* Unchecked increment ok here since no further access on this iteration; +will be checked on next at "R-name". */ + +dnss->aptr += dnss->srr.size; /* Advance to next RR */ + +/* Return a pointer to the dns_record structure within the dns_answer. This is +for convenience so that the scans can use nice-looking for loops. */ + +TRACE debug_printf("%s: return %s\n", __FUNCTION__, dns_text_type(dnss->srr.type)); +return &dnss->srr; + +null_return: + TRACE debug_printf("%s: terminate (%d RRs left). Last op: %s; errno %d %s\n", + __FUNCTION__, dnss->rrcount, trace, errno, strerror(errno)); + dnss->rrcount = 0; + return NULL; +} + + +/* Extract the AUTHORITY information from the answer. If the answer isn't +authoritative (AA not set), we do not extract anything. + +The AUTHORITY section contains NS records if the name in question was found, +it contains a SOA record otherwise. (This is just from experience and some +tests, is there some spec?) + +Scan the whole AUTHORITY section, since it may contain other records +(e.g. NSEC3) too. + +Return: name for the authority, in an allocated string, or NULL if none found */ + +static const uschar * +dns_extract_auth_name(const dns_answer * dnsa) /* FIXME: const dns_answer */ +{ +dns_scan dnss; +const HEADER * h = (const HEADER *) dnsa->answer; + +if (h->nscount && h->aa) + for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_AUTHORITY); + rr; rr = dns_next_rr(dnsa, &dnss, RESET_NEXT)) + if (rr->type == (h->ancount ? T_NS : T_SOA)) + return string_copy(rr->name); +return NULL; +} + + + + +/************************************************* +* Return whether AD bit set in DNS result * +*************************************************/ + +/* We do not perform DNSSEC work ourselves; if the administrator has installed +a verifying resolver which sets AD as appropriate, though, we'll use that. +(AD = Authentic Data, AA = Authoritative Answer) + +Argument: pointer to dns answer block +Returns: bool indicating presence of AD bit +*/ + +BOOL +dns_is_secure(const dns_answer * dnsa) +{ +#ifdef DISABLE_DNSSEC +DEBUG(D_dns) + debug_printf("DNSSEC support disabled at build-time; dns_is_secure() false\n"); +return FALSE; +#else +const HEADER * h = (const HEADER *) dnsa->answer; +const uschar * auth_name; +const uschar * trusted; + +if (dnsa->answerlen < 0) return FALSE; +/* Beware that newer versions of glibc on Linux will filter out the ad bit +unless their shiny new RES_TRUSTAD bit is set for the resolver. */ +if (h->ad) return TRUE; + +/* If the resolver we ask is authoritative for the domain in question, it may +not set the AD but the AA bit. If we explicitly trust the resolver for that +domain (via a domainlist in dns_trust_aa), we return TRUE to indicate a secure +answer. */ + +if ( !h->aa + || !dns_trust_aa + || !(trusted = expand_string(dns_trust_aa)) + || !*trusted + || !(auth_name = dns_extract_auth_name(dnsa)) + || OK != match_isinlist(auth_name, &trusted, 0, &domainlist_anchor, NULL, + MCL_DOMAIN, TRUE, NULL) + ) + return FALSE; + +DEBUG(D_dns) debug_printf("DNS faked the AD bit " + "(got AA and matched with dns_trust_aa (%s in %s))\n", + auth_name, dns_trust_aa); + +return TRUE; +#endif +} + +static void +dns_set_insecure(dns_answer * dnsa) +{ +#ifndef DISABLE_DNSSEC +HEADER * h = (HEADER *)dnsa->answer; +h->aa = h->ad = 0; +#endif +} + +/************************************************ + * Check whether the AA bit is set * + * We need this to warn if we requested AD * + * from an authoritative server * + ************************************************/ + +BOOL +dns_is_aa(const dns_answer * dnsa) +{ +#ifdef DISABLE_DNSSEC +return FALSE; +#else +return dnsa->answerlen >= 0 && ((const HEADER *)dnsa->answer)->aa; +#endif +} + + + +/************************************************* +* Turn DNS type into text * +*************************************************/ + +/* Turn the coded record type into a string for printing. All those that Exim +uses should be included here. + +Argument: record type +Returns: pointer to string +*/ + +uschar * +dns_text_type(int t) +{ +switch(t) + { + case T_A: return US"A"; + case T_MX: return US"MX"; + case T_AAAA: return US"AAAA"; + case T_A6: return US"A6"; + case T_TXT: return US"TXT"; + case T_SPF: return US"SPF"; + case T_PTR: return US"PTR"; + case T_SOA: return US"SOA"; + case T_SRV: return US"SRV"; + case T_NS: return US"NS"; + case T_CNAME: return US"CNAME"; + case T_TLSA: return US"TLSA"; + default: return US"?"; + } +} + + + +/************************************************* +* Cache a failed DNS lookup result * +*************************************************/ + +static void +dns_fail_tag(uschar * buf, const uschar * name, int dns_type) +{ +res_state resp = os_get_dns_resolver_res(); + +/*XX buf needs to be 255 +1 + (max(typetext) == 5) +1 + max(chars_for_long-max) +1 +We truncate the name here for safety... could use a dynamic string. */ + +sprintf(CS buf, "%.255s-%s-%lx", name, dns_text_type(dns_type), + (unsigned long) resp->options); +} + + +/* We cache failed lookup results so as not to experience timeouts many +times for the same domain. We need to retain the resolver options because they +may change. For successful lookups, we rely on resolver and/or name server +caching. + +Arguments: + name the domain name + type the lookup type + expiry time TTL expires, or zero for unlimited + rc the return code + +Returns: the return code +*/ + +/* we need: 255 +1 + (max(typetext) == 5) +1 + max(chars_for_long-max) +1 */ +#define DNS_FAILTAG_MAX 290 +#define DNS_FAILNODE_SIZE \ + (sizeof(expiring_data) + sizeof(tree_node) + DNS_FAILTAG_MAX) + +static int +dns_fail_return(const uschar * name, int type, time_t expiry, int rc) +{ +uschar node_name[DNS_FAILTAG_MAX]; +tree_node * previous, * new; +expiring_data * e; + +dns_fail_tag(node_name, name, type); +if ((previous = tree_search(tree_dns_fails, node_name))) + e = previous->data.ptr; +else + { + e = store_get_perm(DNS_FAILNODE_SIZE, name); + new = (void *)(e+1); + dns_fail_tag(new->name, name, type); + new->data.ptr = e; + (void)tree_insertnode(&tree_dns_fails, new); + } + +DEBUG(D_dns) debug_printf(" %s neg-cache entry for %s, ttl %d\n", + previous ? "update" : "writing", + node_name, expiry ? (int)(expiry - time(NULL)) : -1); +e->expiry = expiry; +e->data.val = rc; +return rc; +} + + +/* Return the cached result of a known-bad lookup, or -1. +*/ +static int +dns_fail_cache_hit(const uschar * name, int type) +{ +uschar node_name[DNS_FAILTAG_MAX]; +tree_node * previous; +expiring_data * e; +int val, rc; + +dns_fail_tag(node_name, name, type); +if (!(previous = tree_search(tree_dns_fails, node_name))) + return -1; + +e = previous->data.ptr; +val = e->data.val; +rc = e->expiry && e->expiry <= time(NULL) ? -1 : val; + +DEBUG(D_dns) debug_printf("DNS lookup of %.255s (%s): %scached value %s%s\n", + name, dns_text_type(type), + rc == -1 ? "" : "using ", + dns_rc_names[val], + rc == -1 ? " past valid time" : ""); + +return rc; +} + + + +/* This is really gross. The successful return value from res_search() is +the packet length, which is stored in dnsa->answerlen. If we get a +negative DNS reply then res_search() returns -1, which causes the bounds +checks for name decompression to fail when it is treated as a packet +length, which in turn causes the authority search to fail. The correct +packet length has been lost inside libresolv, so we have to guess a +replacement value. (The only way to fix this properly would be to +re-implement res_search() and res_query() so that they don't muddle their +success and packet length return values.) For added safety we only reset +the packet length if the packet header looks plausible. + +Return TRUE iff it seemed ok */ + +static BOOL +fake_dnsa_len_for_fail(dns_answer * dnsa, int type) +{ +const HEADER * h = (const HEADER *)dnsa->answer; + +if ( h->qr == 1 /* a response */ + && h->opcode == QUERY + && h->tc == 0 /* nmessage not truncated */ + && (h->rcode == NOERROR || h->rcode == NXDOMAIN) + && ( ntohs(h->qdcount) == 1 /* one question record */ + || f.running_in_test_harness) + && ntohs(h->ancount) == 0 /* no answer records */ + && ntohs(h->nscount) >= 1) /* authority records */ + { + DEBUG(D_dns) debug_printf("faking res_search(%s) response length as %d\n", + dns_text_type(type), (int)sizeof(dnsa->answer)); + dnsa->answerlen = sizeof(dnsa->answer); + return TRUE; + } +DEBUG(D_dns) debug_printf("DNS: couldn't fake dnsa len\n"); +/* Maybe we should just do a second lookup for an SOA? */ +return FALSE; +} + + +/* Return the TTL suitable for an NXDOMAIN result, which is given +in the SOA. We hope that one was returned in the lookup, and do not +bother doing a separate lookup; if not found return a forever TTL. +*/ + +time_t +dns_expire_from_soa(dns_answer * dnsa, int type) +{ +dns_scan dnss; + +if (fake_dnsa_len_for_fail(dnsa, type)) + for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_AUTHORITY); + rr; rr = dns_next_rr(dnsa, &dnss, RESET_NEXT) + ) if (rr->type == T_SOA) + { + const uschar * p = rr->data; + uschar discard_buf[256]; + int len; + unsigned long ttl; + + /* Skip the mname & rname strings */ + + if ((len = dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen, + p, (DN_EXPAND_ARG4_TYPE)discard_buf, 256)) < 0) + break; + p += len; + if ((len = dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen, + p, (DN_EXPAND_ARG4_TYPE)discard_buf, 256)) < 0) + break; + p += len; + + /* Skip the SOA serial, refresh, retry & expire. Grab the TTL */ + + if (p > dnsa->answer + dnsa->answerlen - 5 * INT32SZ) + break; + p += 4 * INT32SZ; + GETLONG(ttl, p); + + return time(NULL) + ttl; + } + +DEBUG(D_dns) debug_printf("DNS: no SOA record found for neg-TTL\n"); +return 0; +} + + +/************************************************* +* Do basic DNS lookup * +*************************************************/ + +/* Call the resolver to look up the given domain name, using the given type, +and check the result. The error code TRY_AGAIN is documented as meaning "non- +Authoritative Host not found, or SERVERFAIL". Sometimes there are badly set +up nameservers that produce this error continually, so there is the option of +providing a list of domains for which this is treated as a non-existent +host. + +The dns_answer structure is pretty big; enough to hold a max-sized DNS message +- so best allocated from fast-release memory. As of writing, all our callers +use a stack-auto variable. + +Arguments: + dnsa pointer to dns_answer structure + name name to look up + type type of DNS record required (T_A, T_MX, etc) + +Returns: DNS_SUCCEED successful lookup + DNS_NOMATCH name not found (NXDOMAIN) + or name contains illegal characters (if checking) + or name is an IP address (for IP address lookup) + DNS_NODATA domain exists, but no data for this type (NODATA) + DNS_AGAIN soft failure, try again later + DNS_FAIL DNS failure +*/ + +int +dns_basic_lookup(dns_answer * dnsa, const uschar * name, int type) +{ +int rc; +#ifndef STAND_ALONE +const uschar * save_domain; +#endif + +/* DNS lookup failures of any kind are cached in a tree. This is mainly so that +a timeout on one domain doesn't happen time and time again for messages that +have many addresses in the same domain. We rely on the resolver and name server +caching for successful lookups. +*/ + +if ((rc = dns_fail_cache_hit(name, type)) > 0) + { + dnsa->answerlen = -1; + return rc; + } + +#ifdef SUPPORT_I18N +/* Convert all names to a-label form before doing lookup */ + { + uschar * alabel; + uschar * errstr = NULL; + DEBUG(D_dns) if (string_is_utf8(name)) + debug_printf("convert utf8 '%s' to alabel for for lookup\n", name); + if ((alabel = string_domain_utf8_to_alabel(name, &errstr)), errstr) + { + DEBUG(D_dns) + debug_printf("DNS name '%s' utf8 conversion to alabel failed: %s\n", name, + errstr); + f.host_find_failed_syntax = TRUE; + return DNS_NOMATCH; + } + name = alabel; + } +#endif + +/* If configured, check the hygiene of the name passed to lookup. Otherwise, +although DNS lookups may give REFUSED at the lower level, some resolvers +turn this into TRY_AGAIN, which is silly. Give a NOMATCH return, since such +domains cannot be in the DNS. The check is now done by a regular expression; +give it space for substring storage to save it having to get its own if the +regex has substrings that are used - the default uses a conditional. + +This test is omitted for PTR records. These occur only in calls from the dnsdb +lookup, which constructs the names itself, so they should be OK. Besides, +bitstring labels don't conform to normal name syntax. (But they aren't used any +more.) */ + +#ifndef STAND_ALONE /* Omit this for stand-alone tests */ + +if (check_dns_names_pattern[0] != 0 && type != T_PTR && type != T_TXT) + { + dns_pattern_init(); + if (!regex_match(regex_check_dns_names, name, -1, NULL)) + { + DEBUG(D_dns) + debug_printf("DNS name syntax check failed: %s (%s)\n", name, + dns_text_type(type)); + f.host_find_failed_syntax = TRUE; + return DNS_NOMATCH; + } + } + +#endif /* STAND_ALONE */ + +/* Call the resolver; for an overlong response, res_search() will return the +number of bytes the message would need, so we need to check for this case. The +effect is to truncate overlong data. + +On some systems, res_search() will recognize "A-for-A" queries and return +the IP address instead of returning -1 with h_error=HOST_NOT_FOUND. Some +nameservers are also believed to do this. It is, of course, contrary to the +specification of the DNS, so we lock it out. */ + +if ((type == T_A || type == T_AAAA) && string_is_ip_address(name, NULL) != 0) + return DNS_NOMATCH; + +/* If we are running in the test harness, instead of calling the normal resolver +(res_search), we call fakens_search(), which recognizes certain special +domains, and interfaces to a fake nameserver for certain special zones. */ + +h_errno = 0; +dnsa->answerlen = f.running_in_test_harness + ? fakens_search(name, type, dnsa->answer, sizeof(dnsa->answer)) + : res_search(CCS name, C_IN, type, dnsa->answer, sizeof(dnsa->answer)); + +if (dnsa->answerlen > (int) sizeof(dnsa->answer)) + { + DEBUG(D_dns) debug_printf("DNS lookup of %s (%s) resulted in overlong packet" + " (size %d), truncating to %u.\n", + name, dns_text_type(type), dnsa->answerlen, (unsigned int) sizeof(dnsa->answer)); + dnsa->answerlen = sizeof(dnsa->answer); + } + +if (dnsa->answerlen < 0) switch (h_errno) + { + case HOST_NOT_FOUND: + DEBUG(D_dns) debug_printf("DNS lookup of %s (%s) gave HOST_NOT_FOUND\n" + "returning DNS_NOMATCH\n", name, dns_text_type(type)); + return dns_fail_return(name, type, dns_expire_from_soa(dnsa, type), DNS_NOMATCH); + + case TRY_AGAIN: + DEBUG(D_dns) debug_printf("DNS lookup of %s (%s) gave TRY_AGAIN\n", + name, dns_text_type(type)); + + /* Cut this out for various test programs */ +#ifndef STAND_ALONE + save_domain = deliver_domain; + deliver_domain = string_copy(name); /* set $domain */ + rc = match_isinlist(name, CUSS &dns_again_means_nonexist, 0, + &domainlist_anchor, NULL, MCL_DOMAIN, TRUE, NULL); + deliver_domain = save_domain; + if (rc != OK) + { + DEBUG(D_dns) debug_printf("returning DNS_AGAIN\n"); + return dns_fail_return(name, type, 0, DNS_AGAIN); + } + DEBUG(D_dns) debug_printf("%s is in dns_again_means_nonexist: returning " + "DNS_NOMATCH\n", name); + return dns_fail_return(name, type, dns_expire_from_soa(dnsa, type), DNS_NOMATCH); + +#else /* For stand-alone tests */ + return dns_fail_return(name, type, 0, DNS_AGAIN); +#endif + + case NO_RECOVERY: + DEBUG(D_dns) debug_printf("DNS lookup of %s (%s) gave NO_RECOVERY\n" + "returning DNS_FAIL\n", name, dns_text_type(type)); + return dns_fail_return(name, type, 0, DNS_FAIL); + + case NO_DATA: + DEBUG(D_dns) debug_printf("DNS lookup of %s (%s) gave NO_DATA\n" + "returning DNS_NODATA\n", name, dns_text_type(type)); + return dns_fail_return(name, type, dns_expire_from_soa(dnsa, type), DNS_NODATA); + + default: + DEBUG(D_dns) debug_printf("DNS lookup of %s (%s) gave unknown DNS error %d\n" + "returning DNS_FAIL\n", name, dns_text_type(type), h_errno); + return dns_fail_return(name, type, 0, DNS_FAIL); + } + +DEBUG(D_dns) debug_printf("DNS lookup of %s (%s) succeeded\n", + name, dns_text_type(type)); + +return DNS_SUCCEED; +} + + + + +/************************************************ +* Do a DNS lookup and handle CNAMES * +************************************************/ + +/* Look up the given domain name, using the given type. Follow CNAMEs if +necessary, but only so many times. There aren't supposed to be CNAME chains in +the DNS, but you are supposed to cope with them if you find them. +By default, follow one CNAME since a resolver has been seen, faced with +an MX request and a CNAME (to an A) but no MX present, returning the CNAME. + +The assumption is made that if the resolver gives back records of the +requested type *and* a CNAME, we don't need to make another call to look up +the CNAME. I can't see how it could return only some of the right records. If +it's done a CNAME lookup in the past, it will have all of them; if not, it +won't return any. + +If fully_qualified_name is not NULL, set it to point to the full name +returned by the resolver, if this is different to what it is given, unless +the returned name starts with "*" as some nameservers seem to be returning +wildcards in this form. In international mode "different" means "alabel +forms are different". + +Arguments: + dnsa pointer to dns_answer structure + name domain name to look up + type DNS record type (T_A, T_MX, etc) + fully_qualified_name if not NULL, return the returned name here if its + contents are different (i.e. it must be preset) + +Returns: DNS_SUCCEED successful lookup + DNS_NOMATCH name not found + DNS_NODATA no data found + DNS_AGAIN soft failure, try again later + DNS_FAIL DNS failure +*/ + +int +dns_lookup(dns_answer *dnsa, const uschar *name, int type, + const uschar **fully_qualified_name) +{ +const uschar *orig_name = name; +BOOL secure_so_far = TRUE; + +/* By default, assume the resolver follows CNAME chains (and returns NODATA for +an unterminated one). If it also does that for a CNAME loop, fine; if it returns +a CNAME (maybe the last?) whine about it. However, retain the coding for dumb +resolvers hiding behind a config variable. Loop to follow CNAME chains so far, +but no further... The testsuite tests the latter case, mostly assuming that the +former will work. */ + +for (int i = 0; i <= dns_cname_loops; i++) + { + uschar * data; + dns_record cname_rr, type_rr; + dns_scan dnss; + int rc; + + /* DNS lookup failures get passed straight back. */ + + if ((rc = dns_basic_lookup(dnsa, name, type)) != DNS_SUCCEED) + return rc; + + /* We should have either records of the required type, or a CNAME record, + or both. We need to know whether both exist for getting the fully qualified + name, but avoid scanning more than necessary. Note that we must copy the + contents of any rr blocks returned by dns_next_rr() as they use the same + area in the dnsa block. */ + + cname_rr.data = type_rr.data = NULL; + for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); + rr; rr = dns_next_rr(dnsa, &dnss, RESET_NEXT)) + if (rr->type == type) + { + if (type_rr.data == NULL) type_rr = *rr; + if (cname_rr.data != NULL) break; + } + else if (rr->type == T_CNAME) + cname_rr = *rr; + + /* For the first time round this loop, if a CNAME was found, take the fully + qualified name from it; otherwise from the first data record, if present. */ + + if (i == 0 && fully_qualified_name) + { + uschar * rr_name = cname_rr.data + ? cname_rr.name : type_rr.data ? type_rr.name : NULL; + if ( rr_name + && Ustrcmp(rr_name, *fully_qualified_name) != 0 + && rr_name[0] != '*' +#ifdef SUPPORT_I18N + && ( !string_is_utf8(*fully_qualified_name) + || Ustrcmp(rr_name, + string_domain_utf8_to_alabel(*fully_qualified_name, NULL)) != 0 + ) +#endif + ) + *fully_qualified_name = string_copy_dnsdomain(rr_name); + } + + /* If any data records of the correct type were found, we are done. */ + + if (type_rr.data) + { + if (!secure_so_far) /* mark insecure if any element of CNAME chain was */ + dns_set_insecure(dnsa); + return DNS_SUCCEED; + } + + /* If there are no data records, we need to re-scan the DNS using the + domain given in the CNAME record, which should exist (otherwise we should + have had a failure from dns_lookup). However code against the possibility of + its not existing. */ + + if (!cname_rr.data) + return DNS_FAIL; + + /* DNS data comes from the outside, hence tainted */ + data = store_get(256, GET_TAINTED); + if (dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen, + cname_rr.data, (DN_EXPAND_ARG4_TYPE)data, 256) < 0) + return DNS_FAIL; + name = data; + + if (!dns_is_secure(dnsa)) + secure_so_far = FALSE; + + DEBUG(D_dns) debug_printf("CNAME found: change to %s\n", name); + } /* Loop back to do another lookup */ + +/*Control reaches here after 10 times round the CNAME loop. Something isn't +right... */ + +log_write(0, LOG_MAIN, "CNAME loop for %s encountered", orig_name); +return DNS_FAIL; +} + + + + + + +/************************************************ +* Do a DNS lookup and handle virtual types * +************************************************/ + +/* This function handles some invented "lookup types" that synthesize features +not available in the basic types. The special types all have negative values. +Positive type values are passed straight on to dns_lookup(). + +Arguments: + dnsa pointer to dns_answer structure + name domain name to look up + type DNS record type (T_A, T_MX, etc or a "special") + fully_qualified_name if not NULL, return the returned name here if its + contents are different (i.e. it must be preset) + +Returns: DNS_SUCCEED successful lookup + DNS_NOMATCH name not found + DNS_NODATA no data found + DNS_AGAIN soft failure, try again later + DNS_FAIL DNS failure +*/ + +int +dns_special_lookup(dns_answer *dnsa, const uschar *name, int type, + const uschar **fully_qualified_name) +{ +switch (type) + { + /* The "mx hosts only" type doesn't require any special action here */ + case T_MXH: + return dns_lookup(dnsa, name, T_MX, fully_qualified_name); + + /* Find nameservers for the domain or the nearest enclosing zone, excluding + the root servers. */ + case T_ZNS: + type = T_NS; + /* FALLTHROUGH */ + case T_SOA: + { + const uschar *d = name; + while (d != 0) + { + int rc = dns_lookup(dnsa, d, type, fully_qualified_name); + if (rc != DNS_NOMATCH && rc != DNS_NODATA) return rc; + while (*d != 0 && *d != '.') d++; + if (*d++ == 0) break; + } + return DNS_NOMATCH; + } + + /* Try to look up the Client SMTP Authorization SRV record for the name. If + there isn't one, search from the top downwards for a CSA record in a parent + domain, which might be making assertions about subdomains. If we find a record + we set fully_qualified_name to whichever lookup succeeded, so that the caller + can tell whether to look at the explicit authorization field or the subdomain + assertion field. */ + case T_CSA: + { + uschar *srvname, *namesuff, *tld; + int priority, dummy_weight, port; + int limit, rc, i; + BOOL ipv6; + dns_record *rr; + dns_scan dnss; + + DEBUG(D_dns) debug_printf("CSA lookup of %s\n", name); + + srvname = string_sprintf("_client._smtp.%s", name); + rc = dns_lookup(dnsa, srvname, T_SRV, NULL); + if (rc == DNS_SUCCEED || rc == DNS_AGAIN) + { + if (rc == DNS_SUCCEED) *fully_qualified_name = string_copy(name); + return rc; + } + + /* Search for CSA subdomain assertion SRV records from the top downwards, + starting with the 2nd level domain. This order maximizes cache-friendliness. + We skip the top level domains to avoid loading their nameservers and because + we know they'll never have CSA SRV records. */ + + namesuff = Ustrrchr(name, '.'); + if (namesuff == NULL) return DNS_NOMATCH; + tld = namesuff + 1; + ipv6 = FALSE; + limit = dns_csa_search_limit; + + /* Use more appropriate search parameters if we are in the reverse DNS. */ + + if (strcmpic(namesuff, US".arpa") == 0) + if (namesuff - 8 > name && strcmpic(namesuff - 8, US".in-addr.arpa") == 0) + { + namesuff -= 8; + tld = namesuff + 1; + limit = 3; + } + else if (namesuff - 4 > name && strcmpic(namesuff - 4, US".ip6.arpa") == 0) + { + namesuff -= 4; + tld = namesuff + 1; + ipv6 = TRUE; + limit = 3; + } + + DEBUG(D_dns) debug_printf("CSA TLD %s\n", tld); + + /* Do not perform the search if the top level or 2nd level domains do not + exist. This is quite common, and when it occurs all the search queries would + go to the root or TLD name servers, which is not friendly. So we check the + AUTHORITY section; if it contains the root's SOA record or the TLD's SOA then + the TLD or the 2LD (respectively) doesn't exist and we can skip the search. + If the TLD and the 2LD exist but the explicit CSA record lookup failed, then + the AUTHORITY SOA will be the 2LD's or a subdomain thereof. */ + + if (rc == DNS_NOMATCH) return DNS_NOMATCH; + + for (i = 0; i < limit; i++) + { + if (ipv6) + { + /* Scan through the IPv6 reverse DNS in chunks of 16 bits worth of IP + address, i.e. 4 hex chars and 4 dots, i.e. 8 chars. */ + namesuff -= 8; + if (namesuff <= name) return DNS_NOMATCH; + } + else + /* Find the start of the preceding domain name label. */ + do + if (--namesuff <= name) return DNS_NOMATCH; + while (*namesuff != '.'); + + DEBUG(D_dns) debug_printf("CSA parent search at %s\n", namesuff + 1); + + srvname = string_sprintf("_client._smtp.%s", namesuff + 1); + rc = dns_lookup(dnsa, srvname, T_SRV, NULL); + if (rc == DNS_AGAIN) return rc; + if (rc != DNS_SUCCEED) continue; + + /* Check that the SRV record we have found is worth returning. We don't + just return the first one we find, because some lower level SRV record + might make stricter assertions than its parent domain. */ + + for (rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); + rr; rr = dns_next_rr(dnsa, &dnss, RESET_NEXT)) if (rr->type == T_SRV) + { + const uschar * p = rr->data; + + /* Extract the numerical SRV fields (p is incremented) */ + GETSHORT(priority, p); + GETSHORT(dummy_weight, p); + GETSHORT(port, p); + + /* Check the CSA version number */ + if (priority != 1) continue; + + /* If it's making an interesting assertion, return this response. */ + if (port & 1) + { + *fully_qualified_name = namesuff + 1; + return DNS_SUCCEED; + } + } + } + return DNS_NOMATCH; + } + + default: + if (type >= 0) + return dns_lookup(dnsa, name, type, fully_qualified_name); + } + +/* Control should never reach here */ + +return DNS_FAIL; +} + + + + + +/************************************************* +* Get address(es) from DNS record * +*************************************************/ + +/* The record type is either T_A for an IPv4 address or T_AAAA for an IPv6 address. + +Argument: + dnsa the DNS answer block + rr the RR + +Returns: pointer to a chain of dns_address items; NULL when the dnsa was overrun +*/ + +dns_address * +dns_address_from_rr(dns_answer *dnsa, dns_record *rr) +{ +dns_address * yield = NULL; +uschar * dnsa_lim = dnsa->answer + dnsa->answerlen; + +if (rr->type == T_A) + { + uschar *p = US rr->data; + if (p + 4 <= dnsa_lim) + { + /* the IP is not regarded as tainted */ + yield = store_get(sizeof(dns_address) + 20, GET_UNTAINTED); + (void)sprintf(CS yield->address, "%d.%d.%d.%d", p[0], p[1], p[2], p[3]); + yield->next = NULL; + } + } + +#if HAVE_IPV6 + +else + { + if (rr->data + 16 <= dnsa_lim) + { + struct in6_addr in6; + for (int i = 0; i < 16; i++) in6.s6_addr[i] = rr->data[i]; + yield = store_get(sizeof(dns_address) + 50, GET_UNTAINTED); + inet_ntop(AF_INET6, &in6, CS yield->address, 50); + yield->next = NULL; + } + } +#endif /* HAVE_IPV6 */ + +return yield; +} + + + +void +dns_pattern_init(void) +{ +if (check_dns_names_pattern[0] != 0 && !regex_check_dns_names) + regex_check_dns_names = + regex_must_compile(check_dns_names_pattern, FALSE, TRUE); +} + +/* vi: aw ai sw=2 +*/ +/* End of dns.c */ diff --git a/src/dnsbl.c b/src/dnsbl.c new file mode 100644 index 0000000..db839af --- /dev/null +++ b/src/dnsbl.c @@ -0,0 +1,651 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Functions concerned with dnsbls */ + + +#include "exim.h" + +/* Structure for caching DNSBL lookups */ + +typedef struct dnsbl_cache_block { + time_t expiry; + dns_address *rhs; + uschar *text; + int rc; + BOOL text_set; +} dnsbl_cache_block; + + +/* Anchor for DNSBL cache */ + +static tree_node *dnsbl_cache = NULL; + + +/* Bits for match_type in one_check_dnsbl() */ + +#define MT_NOT 1 +#define MT_ALL 2 + + +/************************************************* +* Perform a single dnsbl lookup * +*************************************************/ + +/* This function is called from verify_check_dnsbl() below. It is also called +recursively from within itself when domain and domain_txt are different +pointers, in order to get the TXT record from the alternate domain. + +Arguments: + domain the outer dnsbl domain + domain_txt alternate domain to lookup TXT record on success; when the + same domain is to be used, domain_txt == domain (that is, + the pointers must be identical, not just the text) + keydomain the current keydomain (for debug message) + prepend subdomain to lookup (like keydomain, but + reversed if IP address) + iplist the list of matching IP addresses, or NULL for "any" + bitmask true if bitmask matching is wanted + match_type condition for 'succeed' result + 0 => Any RR in iplist (=) + 1 => No RR in iplist (!=) + 2 => All RRs in iplist (==) + 3 => Some RRs not in iplist (!==) + the two bits are defined as MT_NOT and MT_ALL + defer_return what to return for a defer + +Returns: OK if lookup succeeded + FAIL if not +*/ + +static int +one_check_dnsbl(uschar *domain, uschar *domain_txt, uschar *keydomain, + uschar *prepend, uschar *iplist, BOOL bitmask, int match_type, + int defer_return) +{ +dns_answer * dnsa = store_get_dns_answer(); +dns_scan dnss; +tree_node *t; +dnsbl_cache_block *cb; +int old_pool = store_pool; +uschar * query; +int qlen, yield; + +/* Construct the specific query domainname */ + +query = string_sprintf("%s.%s", prepend, domain); +if ((qlen = Ustrlen(query)) >= 256) + { + log_write(0, LOG_MAIN|LOG_PANIC, "dnslist query is too long " + "(ignored): %s...", query); + yield = FAIL; + goto out; + } + +/* Look for this query in the cache. */ + +if ( (t = tree_search(dnsbl_cache, query)) + && (cb = t->data.ptr)->expiry > time(NULL) + ) + +/* Previous lookup was cached */ + + { + HDEBUG(D_dnsbl) debug_printf("dnslists: using result of previous lookup\n"); + } + +/* If not cached from a previous lookup, we must do a DNS lookup, and +cache the result in permanent memory. */ + +else + { + uint ttl = 3600; /* max TTL for positive cache entries */ + + store_pool = POOL_PERM; + + if (t) + { + HDEBUG(D_dnsbl) debug_printf("cached data found but past valid time; "); + } + + else + { /* Set up a tree entry to cache the lookup */ + t = store_get(sizeof(tree_node) + qlen + 1 + 1, query); + Ustrcpy(t->name, query); + t->data.ptr = cb = store_get(sizeof(dnsbl_cache_block), GET_UNTAINTED); + (void)tree_insertnode(&dnsbl_cache, t); + } + + /* Do the DNS lookup . */ + + HDEBUG(D_dnsbl) debug_printf("new DNS lookup for %s\n", query); + cb->rc = dns_basic_lookup(dnsa, query, T_A); + cb->text_set = FALSE; + cb->text = NULL; + cb->rhs = NULL; + + /* If the lookup succeeded, cache the RHS address. The code allows for + more than one address - this was for complete generality and the possible + use of A6 records. However, A6 records are no longer supported. Leave the code + here, just in case. + + Quite apart from one A6 RR generating multiple addresses, there are DNS + lists that return more than one A record, so we must handle multiple + addresses generated in that way as well. + + Mark the cache entry with the "now" plus the minimum of the address TTLs, + or the RFC 2308 negative-cache value from the SOA if none were found. */ + + switch (cb->rc) + { + case DNS_SUCCEED: + { + dns_address ** addrp = &cb->rhs; + dns_address * da; + for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr; + rr = dns_next_rr(dnsa, &dnss, RESET_NEXT)) + if (rr->type == T_A && (da = dns_address_from_rr(dnsa, rr))) + { + *addrp = da; + while (da->next) da = da->next; + addrp = &da->next; + if (ttl > rr->ttl) ttl = rr->ttl; + } + + if (cb->rhs) + { + cb->expiry = time(NULL) + ttl; + break; + } + + /* If we didn't find any A records, change the return code. This can + happen when there is a CNAME record but there are no A records for what + it points to. */ + + cb->rc = DNS_NODATA; + } + /*FALLTHROUGH*/ + + case DNS_NOMATCH: + case DNS_NODATA: + { + /* Although there already is a neg-cache layer maintained by + dns_basic_lookup(), we have a dnslist cache entry allocated and + tree-inserted. So we may as well use it. */ + + time_t soa_negttl = dns_expire_from_soa(dnsa, T_A); + cb->expiry = soa_negttl ? soa_negttl : time(NULL) + ttl; + break; + } + + default: + cb->expiry = time(NULL) + ttl; + break; + } + + store_pool = old_pool; + HDEBUG(D_dnsbl) debug_printf("dnslists: wrote cache entry, ttl=%d\n", + (int)(cb->expiry - time(NULL))); + } + +/* We now have the result of the DNS lookup, either newly done, or cached +from a previous call. If the lookup succeeded, check against the address +list if there is one. This may be a positive equality list (introduced by +"="), a negative equality list (introduced by "!="), a positive bitmask +list (introduced by "&"), or a negative bitmask list (introduced by "!&").*/ + +if (cb->rc == DNS_SUCCEED) + { + dns_address * da = NULL; + uschar *addlist = cb->rhs->address; + + /* For A and AAAA records, there may be multiple addresses from multiple + records. For A6 records (currently not expected to be used) there may be + multiple addresses from a single record. */ + + for (da = cb->rhs->next; da; da = da->next) + addlist = string_sprintf("%s, %s", addlist, da->address); + + HDEBUG(D_dnsbl) debug_printf("DNS lookup for %s succeeded (yielding %s)\n", + query, addlist); + + /* Address list check; this can be either for equality, or via a bitmask. + In the latter case, all the bits must match. */ + + if (iplist) + { + for (da = cb->rhs; da; da = da->next) + { + int ipsep = ','; + const uschar *ptr = iplist; + uschar *res; + + /* Handle exact matching */ + + if (!bitmask) + { + while ((res = string_nextinlist(&ptr, &ipsep, NULL, 0))) + if (Ustrcmp(CS da->address, res) == 0) + break; + } + + /* Handle bitmask matching */ + + else + { + int address[4]; + int mask = 0; + + /* At present, all known DNS blocking lists use A records, with + IPv4 addresses on the RHS encoding the information they return. I + wonder if this will linger on as the last vestige of IPv4 when IPv6 + is ubiquitous? Anyway, for now we use paranoia code to completely + ignore IPv6 addresses. The default mask is 0, which always matches. + We change this only for IPv4 addresses in the list. */ + + if (host_aton(da->address, address) == 1) + if ((address[0] & 0xff000000) != 0x7f000000) /* 127.0.0.0/8 */ + log_write(0, LOG_MAIN, + "DNS list lookup for %s at %s returned %s;" + " not in 127.0/8 and discarded", + keydomain, domain, da->address); + + else + mask = address[0]; + + /* Scan the returned addresses, skipping any that are IPv6 */ + + while ((res = string_nextinlist(&ptr, &ipsep, NULL, 0))) + if (host_aton(res, address) == 1) + if ((address[0] & mask) == address[0]) + break; + } + + /* If either + + (a) An IP address in an any ('=') list matched, or + (b) No IP address in an all ('==') list matched + + then we're done searching. */ + + if (((match_type & MT_ALL) != 0) == (res == NULL)) break; + } + + /* If da == NULL, either + + (a) No IP address in an any ('=') list matched, or + (b) An IP address in an all ('==') list didn't match + + so behave as if the DNSBL lookup had not succeeded, i.e. the host is not on + the list. */ + + if ((match_type == MT_NOT || match_type == MT_ALL) != (da == NULL)) + { + HDEBUG(D_dnsbl) + { + uschar *res = NULL; + switch(match_type) + { + case 0: + res = US"was no match"; break; + case MT_NOT: + res = US"was an exclude match"; break; + case MT_ALL: + res = US"was an IP address that did not match"; break; + case MT_NOT|MT_ALL: + res = US"were no IP addresses that did not match"; break; + } + debug_printf("=> but we are not accepting this block class because\n"); + debug_printf("=> there %s for %s%c%s\n", + res, + match_type & MT_ALL ? "=" : "", + bitmask ? '&' : '=', iplist); + } + yield = FAIL; + goto out; + } + } + + /* No address list check; discard any illegal returns and give up if + none remain. */ + + else + { + BOOL ok = FALSE; + for (da = cb->rhs; da; da = da->next) + { + int address[4]; + + if ( host_aton(da->address, address) == 1 /* ipv4 */ + && (address[0] & 0xff000000) == 0x7f000000 /* 127.0.0.0/8 */ + ) + ok = TRUE; + else + log_write(0, LOG_MAIN, + "DNS list lookup for %s at %s returned %s;" + " not in 127.0/8 and discarded", + keydomain, domain, da->address); + } + if (!ok) + { + yield = FAIL; + goto out; + } + } + + /* Either there was no IP list, or the record matched, implying that the + domain is on the list. We now want to find a corresponding TXT record. If an + alternate domain is specified for the TXT record, call this function + recursively to look that up; this has the side effect of re-checking that + there is indeed an A record at the alternate domain. */ + + if (domain_txt != domain) + { + yield = one_check_dnsbl(domain_txt, domain_txt, keydomain, prepend, NULL, + FALSE, match_type, defer_return); + goto out; + } + + /* If there is no alternate domain, look up a TXT record in the main domain + if it has not previously been cached. */ + + if (!cb->text_set) + { + cb->text_set = TRUE; + if (dns_basic_lookup(dnsa, query, T_TXT) == DNS_SUCCEED) + for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr; + rr = dns_next_rr(dnsa, &dnss, RESET_NEXT)) + if (rr->type == T_TXT) + { + int len = (rr->data)[0]; + if (len > 511) len = 127; + store_pool = POOL_PERM; + cb->text = string_copyn_taint(CUS (rr->data+1), len, GET_TAINTED); + store_pool = old_pool; + break; + } + } + + dnslist_value = addlist; + dnslist_text = cb->text; + yield = OK; + goto out; + } + +/* There was a problem with the DNS lookup */ + +if (cb->rc != DNS_NOMATCH && cb->rc != DNS_NODATA) + { + log_write(L_dnslist_defer, LOG_MAIN, + "DNS list lookup defer (probably timeout) for %s: %s", query, + defer_return == OK ? US"assumed in list" : + defer_return == FAIL ? US"assumed not in list" : + US"returned DEFER"); + yield = defer_return; + goto out; + } + +/* No entry was found in the DNS; continue for next domain */ + +HDEBUG(D_dnsbl) + { + debug_printf("DNS lookup for %s failed\n", query); + debug_printf("=> that means %s is not listed at %s\n", + keydomain, domain); + } + +yield = FAIL; + +out: + +store_free_dns_answer(dnsa); +return yield; +} + + + + +/************************************************* +* Check host against DNS black lists * +*************************************************/ + +/* This function runs checks against a list of DNS black lists, until one +matches. Each item on the list can be of the form + + domain=ip-address/key + +The domain is the right-most domain that is used for the query, for example, +blackholes.mail-abuse.org. If the IP address is present, there is a match only +if the DNS lookup returns a matching IP address. Several addresses may be +given, comma-separated, for example: x.y.z=127.0.0.1,127.0.0.2. + +If no key is given, what is looked up in the domain is the inverted IP address +of the current client host. If a key is given, it is used to construct the +domain for the lookup. For example: + + dsn.rfc-ignorant.org/$sender_address_domain + +After finding a match in the DNS, the domain is placed in $dnslist_domain, and +then we check for a TXT record for an error message, and if found, save its +value in $dnslist_text. We also cache everything in a tree, to optimize +multiple lookups. + +The TXT record is normally looked up in the same domain as the A record, but +when many lists are combined in a single DNS domain, this will not be a very +specific message. It is possible to specify a different domain for looking up +TXT records; this is given before the main domain, comma-separated. For +example: + + dnslists = http.dnsbl.sorbs.net,dnsbl.sorbs.net=127.0.0.2 : \ + socks.dnsbl.sorbs.net,dnsbl.sorbs.net=127.0.0.3 + +The caching ensures that only one lookup in dnsbl.sorbs.net is done. + +Note: an address for testing RBL is 192.203.178.39 +Note: an address for testing DUL is 192.203.178.4 +Note: a domain for testing RFCI is example.tld.dsn.rfc-ignorant.org + +Arguments: + where the acl type + listptr the domain/address/data list + log_msgptr log message on error + +Returns: OK successful lookup (i.e. the address is on the list), or + lookup deferred after +include_unknown + FAIL name not found, or no data found for the given type, or + lookup deferred after +exclude_unknown (default) + DEFER lookup failure, if +defer_unknown was set +*/ + +int +verify_check_dnsbl(int where, const uschar ** listptr, uschar ** log_msgptr) +{ +int sep = 0; +int defer_return = FAIL; +const uschar *list = *listptr; +uschar *domain; +uschar revadd[128]; /* Long enough for IPv6 address */ + +/* Indicate that the inverted IP address is not yet set up */ + +revadd[0] = 0; + +/* In case this is the first time the DNS resolver is being used. */ + +dns_init(FALSE, FALSE, FALSE); /*XXX dnssec? */ + +/* Loop through all the domains supplied, until something matches */ + +while ((domain = string_nextinlist(&list, &sep, NULL, 0))) + { + int rc; + BOOL bitmask = FALSE; + int match_type = 0; + uschar *domain_txt; + uschar *comma; + uschar *iplist; + uschar *key; + + HDEBUG(D_dnsbl) debug_printf("dnslists check: %s\n", domain); + + /* Deal with special values that change the behaviour on defer */ + + if (domain[0] == '+') + { + if (strcmpic(domain, US"+include_unknown") == 0) defer_return = OK; + else if (strcmpic(domain, US"+exclude_unknown") == 0) defer_return = FAIL; + else if (strcmpic(domain, US"+defer_unknown") == 0) defer_return = DEFER; + else + log_write(0, LOG_MAIN|LOG_PANIC, "unknown item in dnslist (ignored): %s", + domain); + continue; + } + + /* See if there's explicit data to be looked up */ + + if ((key = Ustrchr(domain, '/'))) *key++ = 0; + + /* See if there's a list of addresses supplied after the domain name. This is + introduced by an = or a & character; if preceded by = we require all matches + and if preceded by ! we invert the result. */ + + if (!(iplist = Ustrchr(domain, '='))) + { + bitmask = TRUE; + iplist = Ustrchr(domain, '&'); + } + + if (iplist) /* Found either = or & */ + { + if (iplist > domain && iplist[-1] == '!') /* Handle preceding ! */ + { + match_type |= MT_NOT; + iplist[-1] = 0; + } + + *iplist++ = 0; /* Terminate domain, move on */ + + /* If we found = (bitmask == FALSE), check for == or =& */ + + if (!bitmask && (*iplist == '=' || *iplist == '&')) + { + bitmask = *iplist++ == '&'; + match_type |= MT_ALL; + } + } + + + /* If there is a comma in the domain, it indicates that a second domain for + looking up TXT records is provided, before the main domain. Otherwise we must + set domain_txt == domain. */ + + domain_txt = domain; + if ((comma = Ustrchr(domain, ','))) + { + *comma++ = 0; + domain = comma; + } + + /* Check that what we have left is a sensible domain name. There is no reason + why these domains should in fact use the same syntax as hosts and email + domains, but in practice they seem to. However, there is little point in + actually causing an error here, because that would no doubt hold up incoming + mail. Instead, I'll just log it. */ + + for (uschar * s = domain; *s; s++) + if (!isalnum(*s) && *s != '-' && *s != '.' && *s != '_') + { + log_write(0, LOG_MAIN, "dnslists domain \"%s\" contains " + "strange characters - is this right?", domain); + break; + } + + /* Check the alternate domain if present */ + + if (domain_txt != domain) for (uschar * s = domain_txt; *s; s++) + if (!isalnum(*s) && *s != '-' && *s != '.' && *s != '_') + { + log_write(0, LOG_MAIN, "dnslists domain \"%s\" contains " + "strange characters - is this right?", domain_txt); + break; + } + + /* If there is no key string, construct the query by adding the domain name + onto the inverted host address, and perform a single DNS lookup. */ + + if (!key) + { + if (where == ACL_WHERE_NOTSMTP_START || where == ACL_WHERE_NOTSMTP) + { + *log_msgptr = string_sprintf + ("cannot test auto-keyed dnslists condition in %s ACL", + acl_wherenames[where]); + return ERROR; + } + if (!sender_host_address) return FAIL; /* can never match */ + if (revadd[0] == 0) invert_address(revadd, sender_host_address); + rc = one_check_dnsbl(domain, domain_txt, sender_host_address, revadd, + iplist, bitmask, match_type, defer_return); + if (rc == OK) + { + dnslist_domain = string_copy(domain_txt); + dnslist_matched = string_copy(sender_host_address); + HDEBUG(D_dnsbl) debug_printf("=> that means %s is listed at %s\n", + sender_host_address, dnslist_domain); + } + if (rc != FAIL) return rc; /* OK or DEFER */ + } + + /* If there is a key string, it can be a list of domains or IP addresses to + be concatenated with the main domain. */ + + else + { + int keysep = 0; + BOOL defer = FALSE; + uschar *keydomain; + uschar keyrevadd[128]; + + while ((keydomain = string_nextinlist(CUSS &key, &keysep, NULL, 0))) + { + uschar *prepend = keydomain; + + if (string_is_ip_address(keydomain, NULL) != 0) + { + invert_address(keyrevadd, keydomain); + prepend = keyrevadd; + } + + rc = one_check_dnsbl(domain, domain_txt, keydomain, prepend, iplist, + bitmask, match_type, defer_return); + if (rc == OK) + { + dnslist_domain = string_copy(domain_txt); + dnslist_matched = string_copy(keydomain); + HDEBUG(D_dnsbl) debug_printf("=> that means %s is listed at %s\n", + keydomain, dnslist_domain); + return OK; + } + + /* If the lookup deferred, remember this fact. We keep trying the rest + of the list to see if we get a useful result, and if we don't, we return + DEFER at the end. */ + + if (rc == DEFER) defer = TRUE; + } /* continue with next keystring domain/address */ + + if (defer) return DEFER; + } + } /* continue with next dnsdb outer domain */ + +return FAIL; +} + +/* vi: aw ai sw=2 +*/ +/* End of dnsbl.c.c */ diff --git a/src/drtables.c b/src/drtables.c new file mode 100644 index 0000000..513ef6c --- /dev/null +++ b/src/drtables.c @@ -0,0 +1,818 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +#include "exim.h" + +#include + +/* This module contains tables that define the lookup methods and drivers +that are actually included in the binary. Its contents are controlled by +various macros in config.h that ultimately come from Local/Makefile. They are +all described in src/EDITME. */ + + +lookup_info **lookup_list; +int lookup_list_count = 0; + +/* Table of information about all possible authentication mechanisms. All +entries are always present if any mechanism is declared, but the functions are +set to NULL for those that are not compiled into the binary. */ + +#ifdef AUTH_CRAM_MD5 +#include "auths/cram_md5.h" +#endif + +#ifdef AUTH_CYRUS_SASL +#include "auths/cyrus_sasl.h" +#endif + +#ifdef AUTH_DOVECOT +#include "auths/dovecot.h" +#endif + +#ifdef AUTH_EXTERNAL +#include "auths/external.h" +#endif + +#ifdef AUTH_GSASL +#include "auths/gsasl_exim.h" +#endif + +#ifdef AUTH_HEIMDAL_GSSAPI +#include "auths/heimdal_gssapi.h" +#endif + +#ifdef AUTH_PLAINTEXT +#include "auths/plaintext.h" +#endif + +#ifdef AUTH_SPA +#include "auths/spa.h" +#endif + +#ifdef AUTH_TLS +#include "auths/tls.h" +#endif + +auth_info auths_available[] = { + +/* Checking by an expansion condition on plain text */ + +#ifdef AUTH_CRAM_MD5 + { + .driver_name = US"cram_md5", /* lookup name */ + .options = auth_cram_md5_options, + .options_count = &auth_cram_md5_options_count, + .options_block = &auth_cram_md5_option_defaults, + .options_len = sizeof(auth_cram_md5_options_block), + .init = auth_cram_md5_init, + .servercode = auth_cram_md5_server, + .clientcode = auth_cram_md5_client, + .version_report = NULL, + .macros_create = NULL, + }, +#endif + +#ifdef AUTH_CYRUS_SASL + { + .driver_name = US"cyrus_sasl", + .options = auth_cyrus_sasl_options, + .options_count = &auth_cyrus_sasl_options_count, + .options_block = &auth_cyrus_sasl_option_defaults, + .options_len = sizeof(auth_cyrus_sasl_options_block), + .init = auth_cyrus_sasl_init, + .servercode = auth_cyrus_sasl_server, + .clientcode = NULL, + .version_report = auth_cyrus_sasl_version_report, + .macros_create = NULL, + }, +#endif + +#ifdef AUTH_DOVECOT + { + .driver_name = US"dovecot", + .options = auth_dovecot_options, + .options_count = &auth_dovecot_options_count, + .options_block = &auth_dovecot_option_defaults, + .options_len = sizeof(auth_dovecot_options_block), + .init = auth_dovecot_init, + .servercode = auth_dovecot_server, + .clientcode = NULL, + .version_report = NULL, + .macros_create = NULL, + }, +#endif + +#ifdef AUTH_EXTERNAL + { + .driver_name = US"external", + .options = auth_external_options, + .options_count = &auth_external_options_count, + .options_block = &auth_external_option_defaults, + .options_len = sizeof(auth_external_options_block), + .init = auth_external_init, + .servercode = auth_external_server, + .clientcode = auth_external_client, + .version_report = NULL, + .macros_create = NULL, + }, +#endif + +#ifdef AUTH_GSASL + { + .driver_name = US"gsasl", + .options = auth_gsasl_options, + .options_count = &auth_gsasl_options_count, + .options_block = &auth_gsasl_option_defaults, + .options_len = sizeof(auth_gsasl_options_block), + .init = auth_gsasl_init, + .servercode = auth_gsasl_server, + .clientcode = auth_gsasl_client, + .version_report = auth_gsasl_version_report, + .macros_create = auth_gsasl_macros, + }, +#endif + +#ifdef AUTH_HEIMDAL_GSSAPI + { + .driver_name = US"heimdal_gssapi", + .options = auth_heimdal_gssapi_options, + .options_count = &auth_heimdal_gssapi_options_count, + .options_block = &auth_heimdal_gssapi_option_defaults, + .options_len = sizeof(auth_heimdal_gssapi_options_block), + .init = auth_heimdal_gssapi_init, + .servercode = auth_heimdal_gssapi_server, + .clientcode = NULL, + .version_report = auth_heimdal_gssapi_version_report, + .macros_create = NULL, + }, +#endif + +#ifdef AUTH_PLAINTEXT + { + .driver_name = US"plaintext", + .options = auth_plaintext_options, + .options_count = &auth_plaintext_options_count, + .options_block = &auth_plaintext_option_defaults, + .options_len = sizeof(auth_plaintext_options_block), + .init = auth_plaintext_init, + .servercode = auth_plaintext_server, + .clientcode = auth_plaintext_client, + .version_report = NULL, + .macros_create = NULL, + }, +#endif + +#ifdef AUTH_SPA + { + .driver_name = US"spa", + .options = auth_spa_options, + .options_count = &auth_spa_options_count, + .options_block = &auth_spa_option_defaults, + .options_len = sizeof(auth_spa_options_block), + .init = auth_spa_init, + .servercode = auth_spa_server, + .clientcode = auth_spa_client, + .version_report = NULL, + .macros_create = NULL, + }, +#endif + +#ifdef AUTH_TLS + { + .driver_name = US"tls", + .options = auth_tls_options, + .options_count = &auth_tls_options_count, + .options_block = &auth_tls_option_defaults, + .options_len = sizeof(auth_tls_options_block), + .init = auth_tls_init, + .servercode = auth_tls_server, + .clientcode = NULL, + .version_report = NULL, + .macros_create = NULL, + }, +#endif + + { .driver_name = US"" } /* end marker */ +}; + + +/* Tables of information about which routers and transports are included in the +exim binary. */ + +/* Pull in the necessary header files */ + +#include "routers/rf_functions.h" + +#ifdef ROUTER_ACCEPT +#include "routers/accept.h" +#endif + +#ifdef ROUTER_DNSLOOKUP +#include "routers/dnslookup.h" +#endif + +#ifdef ROUTER_MANUALROUTE +#include "routers/manualroute.h" +#endif + +#ifdef ROUTER_IPLITERAL +#include "routers/ipliteral.h" +#endif + +#ifdef ROUTER_IPLOOKUP +#include "routers/iplookup.h" +#endif + +#ifdef ROUTER_QUERYPROGRAM +#include "routers/queryprogram.h" +#endif + +#ifdef ROUTER_REDIRECT +#include "routers/redirect.h" +#endif + +#ifdef TRANSPORT_APPENDFILE +#include "transports/appendfile.h" +#endif + +#ifdef TRANSPORT_AUTOREPLY +#include "transports/autoreply.h" +#endif + +#ifdef TRANSPORT_LMTP +#include "transports/lmtp.h" +#endif + +#ifdef TRANSPORT_PIPE +#include "transports/pipe.h" +#endif + +#ifdef EXPERIMENTAL_QUEUEFILE +#include "transports/queuefile.h" +#endif + +#ifdef TRANSPORT_SMTP +#include "transports/smtp.h" +#endif + + +/* Now set up the structures, terminated by an entry with a null name. */ + +router_info routers_available[] = { +#ifdef ROUTER_ACCEPT + { + .driver_name = US"accept", + .options = accept_router_options, + .options_count = &accept_router_options_count, + .options_block = &accept_router_option_defaults, + .options_len = sizeof(accept_router_options_block), + .init = accept_router_init, + .code = accept_router_entry, + .tidyup = NULL, /* no tidyup entry */ + .ri_flags = ri_yestransport + }, +#endif +#ifdef ROUTER_DNSLOOKUP + { + .driver_name = US"dnslookup", + .options = dnslookup_router_options, + .options_count = &dnslookup_router_options_count, + .options_block = &dnslookup_router_option_defaults, + .options_len = sizeof(dnslookup_router_options_block), + .init = dnslookup_router_init, + .code = dnslookup_router_entry, + .tidyup = NULL, /* no tidyup entry */ + .ri_flags = ri_yestransport + }, +#endif +#ifdef ROUTER_IPLITERAL + { + .driver_name = US"ipliteral", + .options = ipliteral_router_options, + .options_count = &ipliteral_router_options_count, + .options_block = &ipliteral_router_option_defaults, + .options_len = sizeof(ipliteral_router_options_block), + .init = ipliteral_router_init, + .code = ipliteral_router_entry, + .tidyup = NULL, /* no tidyup entry */ + .ri_flags = ri_yestransport + }, +#endif +#ifdef ROUTER_IPLOOKUP + { + .driver_name = US"iplookup", + .options = iplookup_router_options, + .options_count = &iplookup_router_options_count, + .options_block = &iplookup_router_option_defaults, + .options_len = sizeof(iplookup_router_options_block), + .init = iplookup_router_init, + .code = iplookup_router_entry, + .tidyup = NULL, /* no tidyup entry */ + .ri_flags = ri_notransport + }, +#endif +#ifdef ROUTER_MANUALROUTE + { + .driver_name = US"manualroute", + .options = manualroute_router_options, + .options_count = &manualroute_router_options_count, + .options_block = &manualroute_router_option_defaults, + .options_len = sizeof(manualroute_router_options_block), + .init = manualroute_router_init, + .code = manualroute_router_entry, + .tidyup = NULL, /* no tidyup entry */ + .ri_flags = 0 + }, +#endif +#ifdef ROUTER_QUERYPROGRAM + { + .driver_name = US"queryprogram", + .options = queryprogram_router_options, + .options_count = &queryprogram_router_options_count, + .options_block = &queryprogram_router_option_defaults, + .options_len = sizeof(queryprogram_router_options_block), + .init = queryprogram_router_init, + .code = queryprogram_router_entry, + .tidyup = NULL, /* no tidyup entry */ + .ri_flags = 0 + }, +#endif +#ifdef ROUTER_REDIRECT + { + .driver_name = US"redirect", + .options = redirect_router_options, + .options_count = &redirect_router_options_count, + .options_block = &redirect_router_option_defaults, + .options_len = sizeof(redirect_router_options_block), + .init = redirect_router_init, + .code = redirect_router_entry, + .tidyup = NULL, /* no tidyup entry */ + .ri_flags = ri_notransport + }, +#endif + { US"" } +}; + + + +transport_info transports_available[] = { +#ifdef TRANSPORT_APPENDFILE + { + .driver_name = US"appendfile", + .options = appendfile_transport_options, + .options_count = &appendfile_transport_options_count, + .options_block = &appendfile_transport_option_defaults, /* private options defaults */ + .options_len = sizeof(appendfile_transport_options_block), + .init = appendfile_transport_init, + .code = appendfile_transport_entry, + .tidyup = NULL, + .closedown = NULL, + .local = TRUE + }, +#endif +#ifdef TRANSPORT_AUTOREPLY + { + .driver_name = US"autoreply", + .options = autoreply_transport_options, + .options_count = &autoreply_transport_options_count, + .options_block = &autoreply_transport_option_defaults, + .options_len = sizeof(autoreply_transport_options_block), + .init = autoreply_transport_init, + .code = autoreply_transport_entry, + .tidyup = NULL, + .closedown = NULL, + .local = TRUE + }, +#endif +#ifdef TRANSPORT_LMTP + { + .driver_name = US"lmtp", + .options = lmtp_transport_options, + .options_count = &lmtp_transport_options_count, + .options_block = &lmtp_transport_option_defaults, + .options_len = sizeof(lmtp_transport_options_block), + .init = lmtp_transport_init, + .code = lmtp_transport_entry, + .tidyup = NULL, + .closedown = NULL, + .local = TRUE + }, +#endif +#ifdef TRANSPORT_PIPE + { + .driver_name = US"pipe", + .options = pipe_transport_options, + .options_count = &pipe_transport_options_count, + .options_block = &pipe_transport_option_defaults, + .options_len = sizeof(pipe_transport_options_block), + .init = pipe_transport_init, + .code = pipe_transport_entry, + .tidyup = NULL, + .closedown = NULL, + .local = TRUE + }, +#endif +#ifdef EXPERIMENTAL_QUEUEFILE + { + .driver_name = US"queuefile", + .options = queuefile_transport_options, + .options_count = &queuefile_transport_options_count, + .options_block = &queuefile_transport_option_defaults, + .options_len = sizeof(queuefile_transport_options_block), + .init = queuefile_transport_init, + .code = queuefile_transport_entry, + .tidyup = NULL, + .closedown = NULL, + .local = TRUE + }, +#endif +#ifdef TRANSPORT_SMTP + { + .driver_name = US"smtp", + .options = smtp_transport_options, + .options_count = &smtp_transport_options_count, + .options_block = &smtp_transport_option_defaults, + .options_len = sizeof(smtp_transport_options_block), + .init = smtp_transport_init, + .code = smtp_transport_entry, + .tidyup = NULL, + .closedown = smtp_transport_closedown, + .local = FALSE + }, +#endif + { US"" } +}; + +#ifndef MACRO_PREDEF + +gstring * +auth_show_supported(gstring * g) +{ +g = string_cat(g, US"Authenticators:"); +for (auth_info * ai = auths_available; ai->driver_name[0]; ai++) + g = string_fmt_append(g, " %s", ai->driver_name); +return string_cat(g, US"\n"); +} + +gstring * +route_show_supported(gstring * g) +{ +g = string_cat(g, US"Routers:"); +for (router_info * rr = routers_available; rr->driver_name[0]; rr++) + g = string_fmt_append(g, " %s", rr->driver_name); +return string_cat(g, US"\n"); +} + +gstring * +transport_show_supported(gstring * g) +{ +g = string_cat(g, US"Transports:"); +#ifdef TRANSPORT_APPENDFILE + g = string_cat(g, US" appendfile"); + #ifdef SUPPORT_MAILDIR + g = string_cat(g, US"/maildir"); /* damn these subclasses */ + #endif + #ifdef SUPPORT_MAILSTORE + g = string_cat(g, US"/mailstore"); + #endif + #ifdef SUPPORT_MBX + g = string_cat(g, US"/mbx"); + #endif +#endif +#ifdef TRANSPORT_AUTOREPLY + g = string_cat(g, US" autoreply"); +#endif +#ifdef TRANSPORT_LMTP + g = string_cat(g, US" lmtp"); +#endif +#ifdef TRANSPORT_PIPE + g = string_cat(g, US" pipe"); +#endif +#ifdef EXPERIMENTAL_QUEUEFILE + g = string_cat(g, US" queuefile"); +#endif +#ifdef TRANSPORT_SMTP + g = string_cat(g, US" smtp"); +#endif +return string_cat(g, US"\n"); +} + + + +struct lookupmodulestr +{ + void *dl; + struct lookup_module_info *info; + struct lookupmodulestr *next; +}; + +static struct lookupmodulestr *lookupmodules = NULL; + +static void +addlookupmodule(void *dl, struct lookup_module_info *info) +{ +struct lookupmodulestr *p = store_get(sizeof(struct lookupmodulestr), GET_UNTAINTED); + +p->dl = dl; +p->info = info; +p->next = lookupmodules; +lookupmodules = p; +lookup_list_count += info->lookupcount; +} + +/* only valid after lookup_list and lookup_list_count are assigned */ +static void +add_lookup_to_list(lookup_info *info) +{ +/* need to add the lookup to lookup_list, sorted */ +int pos = 0; + +/* strategy is to go through the list until we find +either an empty spot or a name that is higher. +this can't fail because we have enough space. */ + +while (lookup_list[pos] && (Ustrcmp(lookup_list[pos]->name, info->name) <= 0)) + pos++; + +if (lookup_list[pos]) + { + /* need to insert it, so move all the other items up + (last slot is still empty, of course) */ + + memmove(&lookup_list[pos+1], &lookup_list[pos], + sizeof(lookup_info *) * (lookup_list_count-pos-1)); + } +lookup_list[pos] = info; +} + + +/* These need to be at file level for old versions of gcc (2.95.2 reported), + * which give parse errors on an extern in function scope. Each entry needs + * to also be invoked in init_lookup_list() below */ + +#if defined(LOOKUP_CDB) && LOOKUP_CDB!=2 +extern lookup_module_info cdb_lookup_module_info; +#endif +#if defined(LOOKUP_DBM) && LOOKUP_DBM!=2 +extern lookup_module_info dbmdb_lookup_module_info; +#endif +#if defined(LOOKUP_DNSDB) && LOOKUP_DNSDB!=2 +extern lookup_module_info dnsdb_lookup_module_info; +#endif +#if defined(LOOKUP_DSEARCH) && LOOKUP_DSEARCH!=2 +extern lookup_module_info dsearch_lookup_module_info; +#endif +#if defined(LOOKUP_IBASE) && LOOKUP_IBASE!=2 +extern lookup_module_info ibase_lookup_module_info; +#endif +#if defined(LOOKUP_JSON) +extern lookup_module_info json_lookup_module_info; +#endif +#if defined(LOOKUP_LDAP) +extern lookup_module_info ldap_lookup_module_info; +#endif +#if defined(LOOKUP_LSEARCH) && LOOKUP_LSEARCH!=2 +extern lookup_module_info lsearch_lookup_module_info; +#endif +#if defined(LOOKUP_MYSQL) && LOOKUP_MYSQL!=2 +extern lookup_module_info mysql_lookup_module_info; +#endif +#if defined(LOOKUP_NIS) && LOOKUP_NIS!=2 +extern lookup_module_info nis_lookup_module_info; +#endif +#if defined(LOOKUP_NISPLUS) && LOOKUP_NISPLUS!=2 +extern lookup_module_info nisplus_lookup_module_info; +#endif +#if defined(LOOKUP_ORACLE) && LOOKUP_ORACLE!=2 +extern lookup_module_info oracle_lookup_module_info; +#endif +#if defined(LOOKUP_PASSWD) && LOOKUP_PASSWD!=2 +extern lookup_module_info passwd_lookup_module_info; +#endif +#if defined(LOOKUP_PGSQL) && LOOKUP_PGSQL!=2 +extern lookup_module_info pgsql_lookup_module_info; +#endif +#if defined(LOOKUP_REDIS) && LOOKUP_REDIS!=2 +extern lookup_module_info redis_lookup_module_info; +#endif +#if defined(LOOKUP_LMDB) +extern lookup_module_info lmdb_lookup_module_info; +#endif +#if defined(SUPPORT_SPF) +extern lookup_module_info spf_lookup_module_info; +#endif +#if defined(LOOKUP_SQLITE) && LOOKUP_SQLITE!=2 +extern lookup_module_info sqlite_lookup_module_info; +#endif +#if defined(LOOKUP_TESTDB) && LOOKUP_TESTDB!=2 +extern lookup_module_info testdb_lookup_module_info; +#endif +#if defined(LOOKUP_WHOSON) && LOOKUP_WHOSON!=2 +extern lookup_module_info whoson_lookup_module_info; +#endif + +extern lookup_module_info readsock_lookup_module_info; + + +void +init_lookup_list(void) +{ +#ifdef LOOKUP_MODULE_DIR +DIR *dd; +struct dirent *ent; +int countmodules = 0; +int moduleerrors = 0; +#endif +static BOOL lookup_list_init_done = FALSE; +rmark reset_point; + +if (lookup_list_init_done) + return; +reset_point = store_mark(); +lookup_list_init_done = TRUE; + +#if defined(LOOKUP_CDB) && LOOKUP_CDB!=2 +addlookupmodule(NULL, &cdb_lookup_module_info); +#endif + +#if defined(LOOKUP_DBM) && LOOKUP_DBM!=2 +addlookupmodule(NULL, &dbmdb_lookup_module_info); +#endif + +#if defined(LOOKUP_DNSDB) && LOOKUP_DNSDB!=2 +addlookupmodule(NULL, &dnsdb_lookup_module_info); +#endif + +#if defined(LOOKUP_DSEARCH) && LOOKUP_DSEARCH!=2 +addlookupmodule(NULL, &dsearch_lookup_module_info); +#endif + +#if defined(LOOKUP_IBASE) && LOOKUP_IBASE!=2 +addlookupmodule(NULL, &ibase_lookup_module_info); +#endif + +#ifdef LOOKUP_LDAP +addlookupmodule(NULL, &ldap_lookup_module_info); +#endif + +#ifdef LOOKUP_JSON +addlookupmodule(NULL, &json_lookup_module_info); +#endif + +#if defined(LOOKUP_LSEARCH) && LOOKUP_LSEARCH!=2 +addlookupmodule(NULL, &lsearch_lookup_module_info); +#endif + +#if defined(LOOKUP_MYSQL) && LOOKUP_MYSQL!=2 +addlookupmodule(NULL, &mysql_lookup_module_info); +#endif + +#if defined(LOOKUP_NIS) && LOOKUP_NIS!=2 +addlookupmodule(NULL, &nis_lookup_module_info); +#endif + +#if defined(LOOKUP_NISPLUS) && LOOKUP_NISPLUS!=2 +addlookupmodule(NULL, &nisplus_lookup_module_info); +#endif + +#if defined(LOOKUP_ORACLE) && LOOKUP_ORACLE!=2 +addlookupmodule(NULL, &oracle_lookup_module_info); +#endif + +#if defined(LOOKUP_PASSWD) && LOOKUP_PASSWD!=2 +addlookupmodule(NULL, &passwd_lookup_module_info); +#endif + +#if defined(LOOKUP_PGSQL) && LOOKUP_PGSQL!=2 +addlookupmodule(NULL, &pgsql_lookup_module_info); +#endif + +#if defined(LOOKUP_REDIS) && LOOKUP_REDIS!=2 +addlookupmodule(NULL, &redis_lookup_module_info); +#endif + +#ifdef LOOKUP_LMDB +addlookupmodule(NULL, &lmdb_lookup_module_info); +#endif + +#ifdef SUPPORT_SPF +addlookupmodule(NULL, &spf_lookup_module_info); +#endif + +#if defined(LOOKUP_SQLITE) && LOOKUP_SQLITE!=2 +addlookupmodule(NULL, &sqlite_lookup_module_info); +#endif + +#if defined(LOOKUP_TESTDB) && LOOKUP_TESTDB!=2 +addlookupmodule(NULL, &testdb_lookup_module_info); +#endif + +#if defined(LOOKUP_WHOSON) && LOOKUP_WHOSON!=2 +addlookupmodule(NULL, &whoson_lookup_module_info); +#endif + +addlookupmodule(NULL, &readsock_lookup_module_info); + +#ifdef LOOKUP_MODULE_DIR +if (!(dd = exim_opendir(LOOKUP_MODULE_DIR))) + { + DEBUG(D_lookup) debug_printf("Couldn't open %s: not loading lookup modules\n", LOOKUP_MODULE_DIR); + log_write(0, LOG_MAIN, "Couldn't open %s: not loading lookup modules\n", LOOKUP_MODULE_DIR); + } +else + { + const pcre2_code *regex_islookupmod = regex_must_compile( + US"\\." DYNLIB_FN_EXT "$", FALSE, TRUE); + + DEBUG(D_lookup) debug_printf("Loading lookup modules from %s\n", LOOKUP_MODULE_DIR); + while ((ent = readdir(dd))) + { + char * name = ent->d_name; + int len = (int)strlen(name); + if (regex_match(regex_islookupmod, US name, len, NUL)) + { + int pathnamelen = len + (int)strlen(LOOKUP_MODULE_DIR) + 2; + void *dl; + struct lookup_module_info *info; + const char *errormsg; + + /* SRH: am I being paranoid here or what? */ + if (pathnamelen > big_buffer_size) + { + fprintf(stderr, "Loading lookup modules: %s/%s: name too long\n", LOOKUP_MODULE_DIR, name); + log_write(0, LOG_MAIN|LOG_PANIC, "%s/%s: name too long\n", LOOKUP_MODULE_DIR, name); + continue; + } + + /* SRH: snprintf here? */ + sprintf(CS big_buffer, "%s/%s", LOOKUP_MODULE_DIR, name); + + if (!(dl = dlopen(CS big_buffer, RTLD_NOW))) + { + errormsg = dlerror(); + fprintf(stderr, "Error loading %s: %s\n", name, errormsg); + log_write(0, LOG_MAIN|LOG_PANIC, "Error loading lookup module %s: %s\n", name, errormsg); + moduleerrors++; + continue; + } + + /* FreeBSD nsdispatch() can trigger dlerror() errors about + _nss_cache_cycle_prevention_function; we need to clear the dlerror() + state before calling dlsym(), so that any error afterwards only comes + from dlsym(). */ + + errormsg = dlerror(); + + info = (struct lookup_module_info*) dlsym(dl, "_lookup_module_info"); + if ((errormsg = dlerror())) + { + fprintf(stderr, "%s does not appear to be a lookup module (%s)\n", name, errormsg); + log_write(0, LOG_MAIN|LOG_PANIC, "%s does not appear to be a lookup module (%s)\n", name, errormsg); + dlclose(dl); + moduleerrors++; + continue; + } + if (info->magic != LOOKUP_MODULE_INFO_MAGIC) + { + fprintf(stderr, "Lookup module %s is not compatible with this version of Exim\n", name); + log_write(0, LOG_MAIN|LOG_PANIC, "Lookup module %s is not compatible with this version of Exim\n", name); + dlclose(dl); + moduleerrors++; + continue; + } + + addlookupmodule(dl, info); + DEBUG(D_lookup) debug_printf("Loaded \"%s\" (%d lookup types)\n", name, info->lookupcount); + countmodules++; + } + } + store_free((void*)regex_islookupmod); + closedir(dd); + } + +DEBUG(D_lookup) debug_printf("Loaded %d lookup modules\n", countmodules); +#endif + +DEBUG(D_lookup) debug_printf("Total %d lookups\n", lookup_list_count); + +lookup_list = store_malloc(sizeof(lookup_info *) * lookup_list_count); +memset(lookup_list, 0, sizeof(lookup_info *) * lookup_list_count); + +/* now add all lookups to the real list */ +for (struct lookupmodulestr * p = lookupmodules; p; p = p->next) + for (int j = 0; j < p->info->lookupcount; j++) + add_lookup_to_list(p->info->lookups[j]); +store_reset(reset_point); +/* just to be sure */ +lookupmodules = NULL; +} + +#endif /*!MACRO_PREDEF*/ +/* End of drtables.c */ diff --git a/src/dummies.c b/src/dummies.c new file mode 100644 index 0000000..38b514b --- /dev/null +++ b/src/dummies.c @@ -0,0 +1,139 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2009 */ +/* Copyright (c) The Exim Maintainers 2021 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* This file is not part of the main Exim code. There are little bits of test +code for some of Exim's modules, and when they are used, the module they are +testing may call other main Exim functions that are not available and/or +should not be used in a test. The classic case is log_write(). This module +contains dummy versions of such functions - well not really dummies, more like +alternates. */ + +#include +#include +#include +#include + +/* We don't have the full Exim headers dragged in, but this function +is used for debugging output. */ + +extern gstring * string_vformat(gstring *, unsigned, const char *, va_list); + + +/************************************************* +* Handle calls to write the log * +*************************************************/ + +/* The message gets written to stderr when log_write() is called from a +utility. The message always gets '\n' added on the end of it. + +Arguments: + selector not relevant when running a utility + flags not relevant when running a utility + format a printf() format + ... arguments for format + +Returns: nothing +*/ + +void +log_write(unsigned int selector, int flags, char *format, ...) +{ +va_list ap; +va_start(ap, format); +vfprintf(stderr, format, ap); +fprintf(stderr, "\n"); +va_end(ap); +} + + +/************************************************* +* Handle calls to print debug output * +*************************************************/ + +/* The message just gets written to stderr. +We use tainted memory to format into just so that we can handle +tainted arguments. + +Arguments: + format a printf() format + ... arguments for format + +Returns: nothing +*/ + +void +debug_printf(char *format, ...) +{ +va_list ap; +rmark reset_point = store_mark(); +gstring * g = string_get_tainted(1024, TRUE); + +va_start(ap, format); + +if (!string_vformat(g, 0, format, ap)) + { + char * s = "**** debug string overflowed buffer ****\n"; + char * p = CS g->s + g->ptr; + int maxlen = g->size - (int)strlen(s) - 3; + if (p > g->s + maxlen) p = g->s + maxlen; + if (p > g->s && p[-1] != '\n') *p++ = '\n'; + strcpy(p, s); + } + +fprintf(stderr, "%s", string_from_gstring(g)); +fflush(stderr); +store_reset(reset_point); +va_end(ap); +} + + + +/************************************************* +* SIGALRM handler * +*************************************************/ + +extern int sigalrm_seen; + +void +sigalrm_handler(int sig) +{ +sigalrm_seen = TRUE; +} + + + +/************************************************* +* Complete Dummies * +*************************************************/ + +int +header_checkname(void *h, char *name, int len) +{ +return 0; +} + +void +directory_make(char *parent, char *name, int mode, int panic) +{ +} + +void +host_build_sender_fullhost(void) { } + +/* This one isn't needed for test_host */ + +#ifndef TEST_HOST +char * +host_ntoa(int type, const void *arg, char *buffer, int *portptr) +{ +return NULL; +} +#endif + + +/* End of dummies.c */ diff --git a/src/enq.c b/src/enq.c new file mode 100644 index 0000000..f7f8c9c --- /dev/null +++ b/src/enq.c @@ -0,0 +1,123 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2015 */ +/* Copyright (c) The Exim Maintainers 2021 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* Functions concerned with serialization. */ + + +#include "exim.h" + + + + +/************************************************* +* Test for host or ETRN serialization * +*************************************************/ + +/* This function is called when a host is listed for serialization of +connections. It is also called when ETRN is listed for serialization. We open +the misc database and look for a record, which implies an existing connection +or ETRN run. If increasing the count would take us past the given limit +value return FALSE. If not, bump it and return TRUE. If not found, create +one with value 1 and return TRUE. + +Arguments: + key string on which to serialize + lim parallelism limit + +Returns: TRUE if OK to proceed; FALSE otherwise +*/ + + +BOOL +enq_start(uschar *key, unsigned lim) +{ +dbdata_serialize *serial_record; +dbdata_serialize new_record; +open_db dbblock; +open_db *dbm_file; + +DEBUG(D_transport) debug_printf("check serialized: %s\n", key); + +/* Open and lock the waiting information database. The absence of O_CREAT is +deliberate; the dbfn_open() function - which is an Exim function - always tries +to create if it can't open a read/write file. It expects only O_RDWR or +O_RDONLY as its argument. */ + +if (!(dbm_file = dbfn_open(US"misc", O_RDWR, &dbblock, TRUE, TRUE))) + return FALSE; + +/* See if there is a record for this host or queue run; if there is, we cannot +proceed with the connection unless the record is very old. */ + +serial_record = dbfn_read_enforce_length(dbm_file, key, sizeof(dbdata_serialize)); +if (serial_record && time(NULL) - serial_record->time_stamp < 6*60*60) + { + if (serial_record->count >= lim) + { + dbfn_close(dbm_file); + DEBUG(D_transport) debug_printf("outstanding serialization record for %s\n", + key); + return FALSE; + } + new_record.count = serial_record->count + 1; + } +else + new_record.count = 1; + +/* We can proceed - insert a new record or update the old one. */ + +DEBUG(D_transport) debug_printf("write serialization record for %s val %d\n", + key, new_record.count); +dbfn_write(dbm_file, key, &new_record, (int)sizeof(dbdata_serialize)); +dbfn_close(dbm_file); +return TRUE; +} + + + +/************************************************* +* Release serialization * +*************************************************/ + +/* This function is called when a serialized host's connection or serialized +ETRN queue run ends. We open the relevant database and delete its record. + +Arguments: + key the serialization key + +Returns: nothing +*/ + +void +enq_end(uschar *key) +{ +open_db dbblock; +open_db *dbm_file; +dbdata_serialize *serial_record; + +DEBUG(D_transport) debug_printf("end serialized: %s\n", key); + +if ( !(dbm_file = dbfn_open(US"misc", O_RDWR, &dbblock, TRUE, TRUE)) + || !(serial_record = dbfn_read_enforce_length(dbm_file, key, sizeof(dbdata_serialize))) + ) + return; +if (--serial_record->count > 0) + { + DEBUG(D_transport) debug_printf("write serialization record for %s val %d\n", + key, serial_record->count); + dbfn_write(dbm_file, key, serial_record, (int)sizeof(dbdata_serialize)); + } +else + { + DEBUG(D_transport) debug_printf("remove serialization record for %s\n", key); + dbfn_delete(dbm_file, key); + } +dbfn_close(dbm_file); +} + +/* End of enq.c */ diff --git a/src/environment.c b/src/environment.c new file mode 100644 index 0000000..9cb90c8 --- /dev/null +++ b/src/environment.c @@ -0,0 +1,83 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) Heiko Schlittermann 2016 + * hs@schlittermann.de + * See the file NOTICE for conditions of use and distribution. + */ + +#include "exim.h" + +extern char **environ; + +/* The cleanup_environment() function is used during the startup phase +of the Exim process, right after reading the configurations main +part, before any expansions take place. It retains the environment +variables we trust (via the keep_environment option) and allows to +set additional variables (via add_environment). + +Returns: TRUE if successful + FALSE otherwise +*/ + +BOOL +cleanup_environment() +{ +int old_pool = store_pool; +store_pool = POOL_PERM; /* Need perm memory for any created env vars */ + +if (!keep_environment || *keep_environment == '\0') + { + /* From: https://github.com/dovecot/core/blob/master/src/lib/env-util.c#L55 + Try to clear the environment. + a) environ = NULL crashes on OS X. + b) *environ = NULL doesn't work on FreeBSD 7.0. + c) environ = emptyenv doesn't work on Haiku OS + d) environ = calloc() should work everywhere */ + + if (environ) *environ = NULL; + + } +else if (Ustrcmp(keep_environment, "*") != 0) + { + rmark reset_point = store_mark(); + if (environ) for (uschar ** p = USS environ; *p; /* see below */) + { + /* It's considered broken if we do not find the '=', according to + Florian Weimer. For now we ignore such strings. unsetenv() would complain, + getenv() would complain. */ + uschar * eqp = Ustrchr(*p, '='); + + if (eqp) + { + uschar * name = string_copyn(*p, eqp - *p); + + if (OK != match_isinlist(name, CUSS &keep_environment, + 0, NULL, NULL, MCL_NOEXPAND, FALSE, NULL)) + if (os_unsetenv(name) < 0) return FALSE; + else p = USS environ; /* RESTART from the beginning */ + else p++; + } + } + store_reset(reset_point); + } +if (add_environment) + { + uschar * p; + int sep = 0; + const uschar * envlist = add_environment; + + while ((p = string_nextinlist(&envlist, &sep, NULL, 0))) + { + DEBUG(D_expand) debug_printf("adding %s\n", p); + putenv(CS p); + } + } +#ifndef DISABLE_TLS +tls_clean_env(); +#endif + +store_pool = old_pool; +return TRUE; +} diff --git a/src/exicyclog.src b/src/exicyclog.src new file mode 100644 index 0000000..20bf9fc --- /dev/null +++ b/src/exicyclog.src @@ -0,0 +1,350 @@ +#! /bin/sh + +# Copyright (c) University of Cambridge, 1995 - 2015 +# See the file NOTICE for conditions of use and distribution. + +# This script takes the following command line arguments: +# -l dir Log file directory +# -k days Number of days to keep the log files + +# Except when they appear in comments, the following placeholders in this +# source are replaced when it is turned into a runnable script: +# +# CONFIGURE_FILE_USE_NODE +# CONFIGURE_FILE_USE_EUID +# CONFIGURE_FILE +# BIN_DIRECTORY +# EXICYCLOG_MAX +# COMPRESS_COMMAND +# COMPRESS_SUFFIX +# CHOWN_COMMAND +# CHGRP_COMMAND +# CHMOD_COMMAND +# TOUCH_COMMAND +# MV_COMMAND +# RM_COMMAND + +# PROCESSED_FLAG + +# This is a shell script for cycling exim main and reject log files. Each time +# it is run, the files get "shuffled down" by one, the current one (e.g. +# mainlog) becoming mainlog.01, the previous mainlog.01 becoming mainlog.02, +# and so on, up to the limit configured here. When the number to keep is +# greater than 99 (not common, but some people do it), three digits are used +# (e.g. mainlog.001). The same shuffling happens to the reject logs. All +# renamed files with numbers greater than 1 are compressed. + +# This script should be called regularly (e.g. daily) by a root crontab +# entry of the form + +# 1 0 * * * /opt/exim/bin/exicyclog + +# The following lines are generated from Exim's configuration file when +# this source is built into a script, but you can subsequently edit them +# without rebuilding things, as long are you are careful not to overwrite +# the script in the next Exim rebuild/install. "Keep" is the number of old log +# files that are required to be kept. Its value can be overridden by the -k +# command line option. "Compress" and "suffix" define your chosen compression +# method. The others are provided because the location of certain commands +# varies from OS to OS. Sigh. + +keep=EXICYCLOG_MAX +compress=COMPRESS_COMMAND +suffix=COMPRESS_SUFFIX + +chgrp=CHGRP_COMMAND +chmod=CHMOD_COMMAND +chown=CHOWN_COMMAND +mv=MV_COMMAND +rm=RM_COMMAND +touch=TOUCH_COMMAND + +# End of editable lines +######################################################################### + +# Sort out command line options. + +while [ $# -gt 0 ] ; do + case "$1" in + -l) log_file_path=$2 + shift + ;; + -k) keep=$2 + shift + ;; + --version) + echo "`basename $0`: $0" + echo "build: EXIM_RELEASE_VERSIONEXIM_VARIANT_VERSION" + exit 0 + ;; + *) echo "** exicyclog: unknown option $1" + exit 1 + ;; + esac + shift +done + +# Some operating systems have different versions in which the commands live +# in different places. We have a fudge that will search the usual suspects if +# requested. + +for cmd in chgrp chmod chown mv rm touch; do + eval "oldcmd=\$$cmd" + if [ "$oldcmd" != "look_for_it" ] ; then continue ; fi + newcmd=$cmd + for dir in /bin /usr/bin /usr/sbin /usr/etc ; do + if [ -f $dir/$cmd ] ; then + newcmd=$dir/$cmd + break + fi + done + eval $cmd=$newcmd +done + +# See if this installation is using the esoteric "USE_EUID" feature of Exim, +# in which it uses the effective user id as a suffix for the configuration file +# name. In order for this to work, exicyclog must be run under the appropriate +# euid. + +if [ "CONFIGURE_FILE_USE_EUID" = "yes" ]; then + euid=.`id -u` +fi + +# See if this installation is using the esoteric "USE_NODE" feature of Exim, +# in which it uses the host's name as a suffix for the configuration file name. + +if [ "CONFIGURE_FILE_USE_NODE" = "yes" ]; then + hostsuffix=.`uname -n` +fi + +# Now find the configuration file name. This has got complicated because the +# CONFIGURE_FILE value may now be a list of files. The one that is used is the +# first one that exists. Mimic the code in readconf.c by testing first for the +# suffixed file in each case. + +set `awk -F: '{ for (i = 1; i <= NF; i++) print $i }' <\s([+-])(\d\d)(\d\d))?/o; + + my $seconds = mktime $sec, $min, $hour, $day, $month - 1, $year - 1900; + + if (defined $tzs) + { + $seconds -= $tzh * 3600 + $tzm * 60 if $tzs eq "+"; + $seconds += $tzh * 3600 + $tzm * 60 if $tzs eq "-"; + } + + return $seconds; + } + + +# This subroutine processes a single line (in $_) from a log file. Program +# defensively against short lines finding their way into the log. + +my (%saved, %id_list, $pattern); + +my $queue_time = -1; +my $insensitive = 1; +my $invert = 0; +my $related = 0; +my $use_pager = 1; +my $literal = 0; + + +# If using "related" option, have to track extra message IDs +my $related_re=''; +my @Mids = (); + +sub do_line + { + + # Convert syslog lines to mainlog format, as in eximstats. + + if (!/^\d{4}-/o) { $_ =~ s/^.*? exim\b.*?: //o; } + + return unless + my($date,$id) = /^(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d(?:\.\d+)? (?:[+-]\d{4} )?)(?:\[\d+\] )?(\w{6}\-\w{6}\-\w{2})?/o; + + # Handle the case when the log line belongs to a specific message. We save + # lines for specific messages until the message is complete. Then either print + # or discard. + + if (defined $id) + { + $saved{$id} = '' unless defined($saved{$id}); + + # Save up the data for this message in case it becomes interesting later. + + $saved{$id} .= $_; + + # Are we interested in this id ? Short circuit if we already were interested. + + if ($invert) + { + $id_list{$id} = 1 if (!defined($id_list{$id})); + $id_list{$id} = 0 if (($insensitive && /$pattern/io) || /$pattern/o); + } + else + { + if (defined $id_list{$id} || + ($insensitive && /$pattern/io) || /$pattern/o) + { + $id_list{$id} = 1; + get_related_ids($id) if $related; + } + elsif ($related && $related_re) + { + grep_for_related($_, $id); + } + } + + # See if this is a completion for some message. If it is interesting, + # print it, but in any event, throw away what was saved. + + if (index($_, 'Completed') != -1 || + index($_, 'SMTP data timeout') != -1 || + (index($_, 'rejected') != -1 && + /^(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d(?:\.\d+)? (?:[+-]\d{4} )?)(?:\[\d+\] )?\w{6}\-\w{6}\-\w{2} rejected/o)) + { + if ($queue_time != -1 && + $saved{$id} =~ /^(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d ([+-]\d{4} )?)/o) + { + my $old_sec = &seconds($1); + my $sec = &seconds($date); + $id_list{$id} = 0 if $id_list{$id} && $sec - $old_sec <= $queue_time; + } + + print "$saved{$id}\n" if ($id_list{$id}); + delete $id_list{$id}; + delete $saved{$id}; + } + } + + # Handle the case where the log line does not belong to a specific message. + # Print it if it is interesting. + + elsif ( ($invert && (($insensitive && !/$pattern/io) || !/$pattern/o)) || + (!$invert && (($insensitive && /$pattern/io) || /$pattern/o)) ) + { print "$_\n"; } + } + +# Rotated log files are frequently compressed and there are a variety of +# formats it could be compressed with. Rather than use just one that is +# detected and hardcoded at Exim compile time, detect and use what the +# logfile is compressed with on the fly. +# +# List of known compression extensions and their associated commands: +my $compressors = { + gz => { cmd => 'zcat', args => '' }, + bz2 => { cmd => 'bzcat', args => '' }, + xz => { cmd => 'xzcat', args => '' }, + lzma => { cmd => 'lzma', args => '-dc' }, + zst => { cmd => 'zstdcat', args => '' }, +}; +my $csearch = 0; + +sub detect_compressor_bin + { + my $ext = shift(); + my $c = $compressors->{$ext}->{cmd}; + $compressors->{$ext}->{bin} = `which $c 2>/dev/null`; + chomp($compressors->{$ext}->{bin}); + } + +sub detect_compressor_capable + { + my $filename = shift(); + map { &detect_compressor_bin($_) } keys %$compressors + if (!$csearch); + $csearch = 1; + return undef + unless (grep {$filename =~ /\.(?:$_)$/} keys %$compressors); + # Loop through them, figure out which one it detected, + # and build the commandline. + my $cmdline = undef; + foreach my $ext (keys %$compressors) + { + if ($filename =~ /\.(?:$ext)$/) + { + # Just die if compressor not found; if this occurs in the middle of + # two valid files with a lot of matches, error could easily be missed. + die("Didn't find $ext decompressor for $filename\n") + if ($compressors->{$ext}->{bin} eq ''); + $cmdline = $compressors->{$ext}->{bin} ." ". + $compressors->{$ext}->{args}; + last; + } + } + return $cmdline; + } + +sub grep_for_related + { + my ($line,$id) = @_; + $id_list{$id} = 1 if $line =~ m/$related_re/; + } + +sub get_related_ids + { + my ($id) = @_; + push @Mids, $id unless grep /\b$id\b/, @Mids; + my $re = join '|', @Mids; + $related_re = qr/$re/; + } + +# The main program. Extract the pattern and make sure any relevant characters +# are quoted if the -l flag is given. The -t flag gives a time-on-queue value +# which is an additional condition. The -M flag will also display "related" +# loglines (msgid from matched lines is searched in following lines). + +GetOptions( + 'I|sensitive' => sub { $insensitive = 0 }, + 'l|literal' => \$literal, + 'M|related' => \$related, + 't|queue-time=i' => \$queue_time, + 'pager!' => \$use_pager, + 'v|invert' => \$invert, + 'h|help' => sub { pod2usage(-exit => 0, -verbose => 1) }, + 'm|man' => sub { + pod2usage( + -exit => 0, + -verbose => 2, + -noperldoc => system('perldoc -V 2>/dev/null >&2') + ); + }, + 'version' => sub { + print basename($0) . ": $0\n", + "build: EXIM_RELEASE_VERSIONEXIM_VARIANT_VERSION\n", + "perl(runtime): $]\n"; + exit 0; + }, +) and @ARGV or pod2usage; + +$pattern = shift @ARGV; +$pattern = quotemeta $pattern if $literal; + +# Start a pager if output goes to a terminal +if (-t 1 and $use_pager) + { + # for perl >= v5.10.x: foreach ($ENV{PAGER}//(), 'less', 'more') + foreach (defined $ENV{PAGER} ? $ENV{PAGER} : (), 'less', 'more') + { + local $ENV{LESS} .= ' --no-init --quit-if-one-screen'; + open(my $pager, '|-', $_) or next; + select $pager; + last; + } + } + +# If file arguments are given, open each one and process according as it is +# is compressed or not. + +if (@ARGV) + { + foreach (@ARGV) + { + my $filename = $_; + if (-x 'ZCAT_COMMAND' && $filename =~ /\.(?:COMPRESS_SUFFIX)$/o) + { + open(LOG, "ZCAT_COMMAND $filename |") || + die "Unable to zcat $filename: $!\n"; + } + elsif (my $cmdline = &detect_compressor_capable($filename)) + { + open(LOG, "$cmdline $filename |") || + die "Unable to decompress $filename: $!\n"; + } + else + { + open(LOG, "<$filename") || die "Unable to open $filename: $!\n"; + } + do_line() while (); + close(LOG); + } + } + +# If no files are named, process STDIN only + +else { do_line() while (); } + +# At the end of processing all the input, print any uncompleted messages. + +for (keys %id_list) + { + print "+++ $_ has not completed +++\n$saved{$_}\n"; + } + +__END__ + +=head1 NAME + +exigrep - search Exim's main log + +=head1 SYNOPSIS + +B [options] pattern [log] ... + +=head1 DESCRIPTION + +The B utility is a Perl script that searches one or more main log +files for entries that match a given pattern. When it finds a match, +it extracts all the log entries for the relevant message, not just +those that match the pattern. Thus, B can extract complete log +entries for a given message, or all mail for a given user, or for a +given host, for example. + +If no file names are given on the command line, the standard input is read. + +For known file extensions indicating compression (F<.gz>, F<.bz2>, F<.xz>, +F<.lzma>, and F<.zst>) a suitable de-compressor is used, if available. + +The output is sent through a pager if a terminal is connected to STDOUT. As +pager are considered: C<$ENV{PAGER}>, C, C. + +=head1 OPTIONS + +=over + +=item B<-l>|B<--literal> + +This means 'literal', that is, treat all characters in the +pattern as standing for themselves. Otherwise the pattern must be a +Perl regular expression. The pattern match is case-insensitive. + +=item B<-t>|B<--queue-time> I + +Limit the output to messages that spent at least I in the +queue. + +=item B<-I>|B<--sensitive> + +Do a case sensitive search. + +=item B<-v>|B<--invert> + +Invert the meaning of the search pattern. That is, print message log +entries that are not related to that pattern. + +=item B<-M>|B<--related> + +Search for related messages too. + +=item B<--no-pager> + +Do not use a pager, even if STDOUT is connected to a terminal. + +=item B<-h>|B<--help> + +Print a short reference help. For more detailed help try L, +or C. + +=item B<-m>|B<--man> + +Print this manual page of B. + +=back + +=head1 SEE ALSO + +L, L, L + +=head1 AUTHOR + +This manual page was stitched together from spec.txt by Andreas Metzler L +and updated by Heiko Schlittermann L. + +=cut diff --git a/src/exim.c b/src/exim.c new file mode 100644 index 0000000..fd01d13 --- /dev/null +++ b/src/exim.c @@ -0,0 +1,6097 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +/* The main function: entry point, initialization, and high-level control. +Also a few functions that don't naturally fit elsewhere. */ + + +#include "exim.h" + +#if defined(__GLIBC__) && !defined(__UCLIBC__) +# include +#endif + +#ifdef USE_GNUTLS +# include +# if GNUTLS_VERSION_NUMBER < 0x030103 && !defined(DISABLE_OCSP) +# define DISABLE_OCSP +# endif +#endif + +#ifndef _TIME_H +# include +#endif + +extern void init_lookup_list(void); + + + +/************************************************* +* Function interface to store functions * +*************************************************/ + +/* We need some real functions to pass to the PCRE regular expression library +for store allocation via Exim's store manager. The normal calls are actually +macros that pass over location information to make tracing easier. These +functions just interface to the standard macro calls. A good compiler will +optimize out the tail recursion and so not make them too expensive. */ + +static void * +function_store_malloc(PCRE2_SIZE size, void * tag) +{ +return store_malloc((int)size); +} + +static void +function_store_free(void * block, void * tag) +{ +/* At least some version of pcre2 pass a null pointer */ +if (block) store_free(block); +} + + + + +/************************************************* +* Enums for cmdline interface * +*************************************************/ + +enum commandline_info { CMDINFO_NONE=0, + CMDINFO_HELP, CMDINFO_SIEVE, CMDINFO_DSCP }; + + + + +/************************************************* +* Compile regular expression and panic on fail * +*************************************************/ + +/* This function is called when failure to compile a regular expression leads +to a panic exit. In other cases, pcre_compile() is called directly. In many +cases where this function is used, the results of the compilation are to be +placed in long-lived store, so we temporarily reset the store management +functions that PCRE uses if the use_malloc flag is set. + +Argument: + pattern the pattern to compile + caseless TRUE if caseless matching is required + use_malloc TRUE if compile into malloc store + +Returns: pointer to the compiled pattern +*/ + +const pcre2_code * +regex_must_compile(const uschar * pattern, BOOL caseless, BOOL use_malloc) +{ +size_t offset; +int options = caseless ? PCRE_COPT|PCRE2_CASELESS : PCRE_COPT; +const pcre2_code * yield; +int err; +pcre2_general_context * gctx; +pcre2_compile_context * cctx; + +if (use_malloc) + { + gctx = pcre2_general_context_create(function_store_malloc, function_store_free, NULL); + cctx = pcre2_compile_context_create(gctx); + } +else + cctx = pcre_cmp_ctx; + +if (!(yield = pcre2_compile((PCRE2_SPTR)pattern, PCRE2_ZERO_TERMINATED, options, + &err, &offset, cctx))) + { + uschar errbuf[128]; + pcre2_get_error_message(err, errbuf, sizeof(errbuf)); + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "regular expression error: " + "%s at offset %ld while compiling %s", errbuf, (long)offset, pattern); + } + +if (use_malloc) + { + pcre2_compile_context_free(cctx); + pcre2_general_context_free(gctx); + } +return yield; +} + + +static void +pcre_init(void) +{ +pcre_gen_ctx = pcre2_general_context_create(function_store_malloc, function_store_free, NULL); +pcre_cmp_ctx = pcre2_compile_context_create(pcre_gen_ctx); +pcre_mtc_ctx = pcre2_match_context_create(pcre_gen_ctx); +} + + + + +/************************************************* +* Execute regular expression and set strings * +*************************************************/ + +/* This function runs a regular expression match, and sets up the pointers to +the matched substrings. The matched strings are copied so the lifetime of +the subject is not a problem. + +Arguments: + re the compiled expression + subject the subject string + options additional PCRE options + setup if < 0 do full setup + if >= 0 setup from setup+1 onwards, + excluding the full matched string + +Returns: TRUE if matched, or FALSE +*/ + +BOOL +regex_match_and_setup(const pcre2_code * re, const uschar * subject, int options, int setup) +{ +pcre2_match_data * md = pcre2_match_data_create_from_pattern(re, pcre_gen_ctx); +int res = pcre2_match(re, (PCRE2_SPTR)subject, PCRE2_ZERO_TERMINATED, 0, + PCRE_EOPT | options, md, pcre_mtc_ctx); +BOOL yield; + +if ((yield = (res >= 0))) + { + res = pcre2_get_ovector_count(md); + expand_nmax = setup < 0 ? 0 : setup + 1; + for (int matchnum = setup < 0 ? 0 : 1; matchnum < res; matchnum++) + { + PCRE2_SIZE len; + pcre2_substring_get_bynumber(md, matchnum, + (PCRE2_UCHAR **)&expand_nstring[expand_nmax], &len); + expand_nlength[expand_nmax++] = (int)len; + } + expand_nmax--; + } +else if (res != PCRE2_ERROR_NOMATCH) DEBUG(D_any) + { + uschar errbuf[128]; + pcre2_get_error_message(res, errbuf, sizeof(errbuf)); + debug_printf_indent("pcre2: %s\n", errbuf); + } +pcre2_match_data_free(md); +return yield; +} + + +/* Check just for match with regex. Uses the common memory-handling. + +Arguments: + re compiled regex + subject string to be checked + slen length of subject; -1 for nul-terminated + rptr pointer for matched string, copied, or NULL + +Return: TRUE for a match. +*/ + +BOOL +regex_match(const pcre2_code * re, const uschar * subject, int slen, uschar ** rptr) +{ +pcre2_match_data * md = pcre2_match_data_create(1, pcre_gen_ctx); +int rc = pcre2_match(re, (PCRE2_SPTR)subject, + slen >= 0 ? slen : PCRE2_ZERO_TERMINATED, + 0, PCRE_EOPT, md, pcre_mtc_ctx); +PCRE2_SIZE * ovec = pcre2_get_ovector_pointer(md); +if (rc < 0) + return FALSE; +if (rptr) + *rptr = string_copyn(subject + ovec[0], ovec[1] - ovec[0]); +return TRUE; +} + + + +/************************************************* +* Set up processing details * +*************************************************/ + +/* Save a text string for dumping when SIGUSR1 is received. +Do checks for overruns. + +Arguments: format and arguments, as for printf() +Returns: nothing +*/ + +void +set_process_info(const char *format, ...) +{ +gstring gs = { .size = PROCESS_INFO_SIZE - 2, .ptr = 0, .s = process_info }; +gstring * g; +int len; +va_list ap; + +g = string_fmt_append(&gs, "%5d ", (int)getpid()); +len = g->ptr; +va_start(ap, format); +if (!string_vformat(g, 0, format, ap)) + { + gs.ptr = len; + g = string_cat(&gs, US"**** string overflowed buffer ****"); + } +g = string_catn(g, US"\n", 1); +string_from_gstring(g); +process_info_len = g->ptr; +DEBUG(D_process_info) debug_printf("set_process_info: %s", process_info); +va_end(ap); +} + +/*********************************************** +* Handler for SIGTERM * +***********************************************/ + +static void +term_handler(int sig) +{ +exit(1); +} + + +/*********************************************** +* Handler for SIGSEGV * +***********************************************/ + +static void +#ifdef SA_SIGINFO +segv_handler(int sig, siginfo_t * info, void * uctx) +{ +log_write(0, LOG_MAIN|LOG_PANIC, "SIGSEGV (fault address: %p)", info->si_addr); +# if defined(SEGV_MAPERR) && defined(SEGV_ACCERR) && defined(SEGV_BNDERR) && defined(SEGV_PKUERR) +switch (info->si_code) + { + case SEGV_MAPERR: log_write(0, LOG_MAIN|LOG_PANIC, "SEGV_MAPERR"); break; + case SEGV_ACCERR: log_write(0, LOG_MAIN|LOG_PANIC, "SEGV_ACCERR"); break; + case SEGV_BNDERR: log_write(0, LOG_MAIN|LOG_PANIC, "SEGV_BNDERR"); break; + case SEGV_PKUERR: log_write(0, LOG_MAIN|LOG_PANIC, "SEGV_PKUERR"); break; + } +# endif +if (US info->si_addr < US 4096) + log_write(0, LOG_MAIN|LOG_PANIC, "SIGSEGV (null pointer indirection)"); +else + log_write(0, LOG_MAIN|LOG_PANIC, "SIGSEGV (maybe attempt to write to immutable memory)"); +if (process_info_len > 0) + log_write(0, LOG_MAIN|LOG_PANIC, "SIGSEGV (%.*s)", process_info_len, process_info); +signal(SIGSEGV, SIG_DFL); +kill(getpid(), sig); +} + +#else +segv_handler(int sig) +{ +log_write(0, LOG_MAIN|LOG_PANIC, "SIGSEGV (maybe attempt to write to immutable memory)"); +if (process_info_len > 0) + log_write(0, LOG_MAIN|LOG_PANIC, "SIGSEGV (%.*s)", process_info_len, process_info); +signal(SIGSEGV, SIG_DFL); +kill(getpid(), sig); +} +#endif + + +/************************************************* +* Handler for SIGUSR1 * +*************************************************/ + +/* SIGUSR1 causes any exim process to write to the process log details of +what it is currently doing. It will only be used if the OS is capable of +setting up a handler that causes automatic restarting of any system call +that is in progress at the time. + +This function takes care to be signal-safe. + +Argument: the signal number (SIGUSR1) +Returns: nothing +*/ + +static void +usr1_handler(int sig) +{ +int fd; + +os_restarting_signal(sig, usr1_handler); + +if (!process_log_path) return; +fd = log_open_as_exim(process_log_path); + +/* If we are neither exim nor root, or if we failed to create the log file, +give up. There is not much useful we can do with errors, since we don't want +to disrupt whatever is going on outside the signal handler. */ + +if (fd < 0) return; + +(void)write(fd, process_info, process_info_len); +(void)close(fd); +} + + + +/************************************************* +* Timeout handler * +*************************************************/ + +/* This handler is enabled most of the time that Exim is running. The handler +doesn't actually get used unless alarm() has been called to set a timer, to +place a time limit on a system call of some kind. When the handler is run, it +re-enables itself. + +There are some other SIGALRM handlers that are used in special cases when more +than just a flag setting is required; for example, when reading a message's +input. These are normally set up in the code module that uses them, and the +SIGALRM handler is reset to this one afterwards. + +Argument: the signal value (SIGALRM) +Returns: nothing +*/ + +void +sigalrm_handler(int sig) +{ +sigalrm_seen = TRUE; +os_non_restarting_signal(SIGALRM, sigalrm_handler); +} + + + +/************************************************* +* Sleep for a fractional time interval * +*************************************************/ + +/* This function is called by millisleep() and exim_wait_tick() to wait for a +period of time that may include a fraction of a second. The coding is somewhat +tedious. We do not expect setitimer() ever to fail, but if it does, the process +will wait for ever, so we panic in this instance. (There was a case of this +when a bug in a function that calls milliwait() caused it to pass invalid data. +That's when I added the check. :-) + +We assume it to be not worth sleeping for under 50us; this value will +require revisiting as hardware advances. This avoids the issue of +a zero-valued timer setting meaning "never fire". + +Argument: an itimerval structure containing the interval +Returns: nothing +*/ + +static void +milliwait(struct itimerval *itval) +{ +sigset_t sigmask; +sigset_t old_sigmask; +int save_errno = errno; + +if (itval->it_value.tv_usec < 50 && itval->it_value.tv_sec == 0) + return; +(void)sigemptyset(&sigmask); /* Empty mask */ +(void)sigaddset(&sigmask, SIGALRM); /* Add SIGALRM */ +(void)sigprocmask(SIG_BLOCK, &sigmask, &old_sigmask); /* Block SIGALRM */ +if (setitimer(ITIMER_REAL, itval, NULL) < 0) /* Start timer */ + log_write(0, LOG_MAIN|LOG_PANIC_DIE, + "setitimer() failed: %s", strerror(errno)); +(void)sigfillset(&sigmask); /* All signals */ +(void)sigdelset(&sigmask, SIGALRM); /* Remove SIGALRM */ +(void)sigsuspend(&sigmask); /* Until SIGALRM */ +(void)sigprocmask(SIG_SETMASK, &old_sigmask, NULL); /* Restore mask */ +errno = save_errno; +sigalrm_seen = FALSE; +} + + + + +/************************************************* +* Millisecond sleep function * +*************************************************/ + +/* The basic sleep() function has a granularity of 1 second, which is too rough +in some cases - for example, when using an increasing delay to slow down +spammers. + +Argument: number of millseconds +Returns: nothing +*/ + +void +millisleep(int msec) +{ +struct itimerval itval = {.it_interval = {.tv_sec = 0, .tv_usec = 0}, + .it_value = {.tv_sec = msec/1000, + .tv_usec = (msec % 1000) * 1000}}; +milliwait(&itval); +} + + + +/************************************************* +* Compare microsecond times * +*************************************************/ + +/* +Arguments: + tv1 the first time + tv2 the second time + +Returns: -1, 0, or +1 +*/ + +static int +exim_tvcmp(struct timeval *t1, struct timeval *t2) +{ +if (t1->tv_sec > t2->tv_sec) return +1; +if (t1->tv_sec < t2->tv_sec) return -1; +if (t1->tv_usec > t2->tv_usec) return +1; +if (t1->tv_usec < t2->tv_usec) return -1; +return 0; +} + + + + +/************************************************* +* Clock tick wait function * +*************************************************/ + +#ifdef _POSIX_MONOTONIC_CLOCK +# ifdef CLOCK_BOOTTIME +# define EXIM_CLOCKTYPE CLOCK_BOOTTIME +# else +# define EXIM_CLOCKTYPE CLOCK_MONOTONIC +# endif + +/* Amount EXIM_CLOCK is behind realtime, at startup. */ +static struct timespec offset_ts; + +static void +exim_clock_init(void) +{ +struct timeval tv; +if (clock_gettime(EXIM_CLOCKTYPE, &offset_ts) != 0) return; +(void)gettimeofday(&tv, NULL); +offset_ts.tv_sec = tv.tv_sec - offset_ts.tv_sec; +offset_ts.tv_nsec = tv.tv_usec * 1000 - offset_ts.tv_nsec; +if (offset_ts.tv_nsec >= 0) return; +offset_ts.tv_sec--; +offset_ts.tv_nsec += 1000*1000*1000; +} +#endif + + +void +exim_gettime(struct timeval * tv) +{ +#ifdef _POSIX_MONOTONIC_CLOCK +struct timespec now_ts; + +if (clock_gettime(EXIM_CLOCKTYPE, &now_ts) == 0) + { + now_ts.tv_sec += offset_ts.tv_sec; + if ((now_ts.tv_nsec += offset_ts.tv_nsec) >= 1000*1000*1000) + { + now_ts.tv_sec++; + now_ts.tv_nsec -= 1000*1000*1000; + } + tv->tv_sec = now_ts.tv_sec; + tv->tv_usec = now_ts.tv_nsec / 1000; + } +else +#endif + (void)gettimeofday(tv, NULL); +} + + +/* Exim uses a time + a pid to generate a unique identifier in two places: its +message IDs, and in file names for maildir deliveries. Because some OS now +re-use pids within the same second, sub-second times are now being used. +However, for absolute certainty, we must ensure the clock has ticked before +allowing the relevant process to complete. At the time of implementation of +this code (February 2003), the speed of processors is such that the clock will +invariably have ticked already by the time a process has done its job. This +function prepares for the time when things are faster - and it also copes with +clocks that go backwards. + +Arguments: + prev_tv A timeval which was used to create uniqueness; its usec field + has been rounded down to the value of the resolution. + We want to be sure the current time is greater than this. + On return, updated to current (rounded down). + resolution The resolution that was used to divide the microseconds + (1 for maildir, larger for message ids) + +Returns: nothing +*/ + +void +exim_wait_tick(struct timeval * prev_tv, int resolution) +{ +struct timeval now_tv; +long int now_true_usec; + +exim_gettime(&now_tv); +now_true_usec = now_tv.tv_usec; +now_tv.tv_usec = (now_true_usec/resolution) * resolution; + +while (exim_tvcmp(&now_tv, prev_tv) <= 0) + { + struct itimerval itval; + itval.it_interval.tv_sec = 0; + itval.it_interval.tv_usec = 0; + itval.it_value.tv_sec = prev_tv->tv_sec - now_tv.tv_sec; + itval.it_value.tv_usec = prev_tv->tv_usec + resolution - now_true_usec; + + /* We know that, overall, "now" is less than or equal to "then". Therefore, a + negative value for the microseconds is possible only in the case when "now" + is more than a second less than "tgt". That means that itval.it_value.tv_sec + is greater than zero. The following correction is therefore safe. */ + + if (itval.it_value.tv_usec < 0) + { + itval.it_value.tv_usec += 1000000; + itval.it_value.tv_sec -= 1; + } + + DEBUG(D_transport|D_receive) + { + if (!f.running_in_test_harness) + { + debug_printf("tick check: " TIME_T_FMT ".%06lu " TIME_T_FMT ".%06lu\n", + prev_tv->tv_sec, (long) prev_tv->tv_usec, + now_tv.tv_sec, (long) now_tv.tv_usec); + debug_printf("waiting " TIME_T_FMT ".%06lu sec\n", + itval.it_value.tv_sec, (long) itval.it_value.tv_usec); + } + } + + milliwait(&itval); + + /* Be prapared to go around if the kernel does not implement subtick + granularity (GNU Hurd) */ + + exim_gettime(&now_tv); + now_true_usec = now_tv.tv_usec; + now_tv.tv_usec = (now_true_usec/resolution) * resolution; + } +*prev_tv = now_tv; +} + + + + +/************************************************* +* Call fopen() with umask 777 and adjust mode * +*************************************************/ + +/* Exim runs with umask(0) so that files created with open() have the mode that +is specified in the open() call. However, there are some files, typically in +the spool directory, that are created with fopen(). They end up world-writeable +if no precautions are taken. Although the spool directory is not accessible to +the world, this is an untidiness. So this is a wrapper function for fopen() +that sorts out the mode of the created file. + +Arguments: + filename the file name + options the fopen() options + mode the required mode + +Returns: the fopened FILE or NULL +*/ + +FILE * +modefopen(const uschar *filename, const char *options, mode_t mode) +{ +mode_t saved_umask = umask(0777); +FILE *f = Ufopen(filename, options); +(void)umask(saved_umask); +if (f != NULL) (void)fchmod(fileno(f), mode); +return f; +} + + +/************************************************* +* Ensure stdin, stdout, and stderr exist * +*************************************************/ + +/* Some operating systems grumble if an exec() happens without a standard +input, output, and error (fds 0, 1, 2) being defined. The worry is that some +file will be opened and will use these fd values, and then some other bit of +code will assume, for example, that it can write error messages to stderr. +This function ensures that fds 0, 1, and 2 are open if they do not already +exist, by connecting them to /dev/null. + +This function is also used to ensure that std{in,out,err} exist at all times, +so that if any library that Exim calls tries to use them, it doesn't crash. + +Arguments: None +Returns: Nothing +*/ + +void +exim_nullstd(void) +{ +int devnull = -1; +struct stat statbuf; +for (int i = 0; i <= 2; i++) + { + if (fstat(i, &statbuf) < 0 && errno == EBADF) + { + if (devnull < 0) devnull = open("/dev/null", O_RDWR); + if (devnull < 0) log_write(0, LOG_MAIN|LOG_PANIC_DIE, "%s", + string_open_failed("/dev/null", NULL)); + if (devnull != i) (void)dup2(devnull, i); + } + } +if (devnull > 2) (void)close(devnull); +} + + + + +/************************************************* +* Close unwanted file descriptors for delivery * +*************************************************/ + +/* This function is called from a new process that has been forked to deliver +an incoming message, either directly, or using exec. + +We want any smtp input streams to be closed in this new process. However, it +has been observed that using fclose() here causes trouble. When reading in -bS +input, duplicate copies of messages have been seen. The files will be sharing a +file pointer with the parent process, and it seems that fclose() (at least on +some systems - I saw this on Solaris 2.5.1) messes with that file pointer, at +least sometimes. Hence we go for closing the underlying file descriptors. + +If TLS is active, we want to shut down the TLS library, but without molesting +the parent's SSL connection. + +For delivery of a non-SMTP message, we want to close stdin and stdout (and +stderr unless debugging) because the calling process might have set them up as +pipes and be waiting for them to close before it waits for the submission +process to terminate. If they aren't closed, they hold up the calling process +until the initial delivery process finishes, which is not what we want. + +Exception: We do want it for synchronous delivery! + +And notwithstanding all the above, if D_resolver is set, implying resolver +debugging, leave stdout open, because that's where the resolver writes its +debugging output. + +When we close stderr (which implies we've also closed stdout), we also get rid +of any controlling terminal. + +Arguments: None +Returns: Nothing +*/ + +static void +close_unwanted(void) +{ +if (smtp_input) + { +#ifndef DISABLE_TLS + tls_close(NULL, TLS_NO_SHUTDOWN); /* Shut down the TLS library */ +#endif + (void)close(fileno(smtp_in)); + (void)close(fileno(smtp_out)); + smtp_in = NULL; + } +else + { + (void)close(0); /* stdin */ + if ((debug_selector & D_resolver) == 0) (void)close(1); /* stdout */ + if (debug_selector == 0) /* stderr */ + { + if (!f.synchronous_delivery) + { + (void)close(2); + log_stderr = NULL; + } + (void)setsid(); + } + } +} + + + + +/************************************************* +* Set uid and gid * +*************************************************/ + +/* This function sets a new uid and gid permanently, optionally calling +initgroups() to set auxiliary groups. There are some special cases when running +Exim in unprivileged modes. In these situations the effective uid will not be +root; if we already have the right effective uid/gid, and don't need to +initialize any groups, leave things as they are. + +Arguments: + uid the uid + gid the gid + igflag TRUE if initgroups() wanted + msg text to use in debugging output and failure log + +Returns: nothing; bombs out on failure +*/ + +void +exim_setugid(uid_t uid, gid_t gid, BOOL igflag, const uschar * msg) +{ +uid_t euid = geteuid(); +gid_t egid = getegid(); + +if (euid == root_uid || euid != uid || egid != gid || igflag) + { + /* At least one OS returns +1 for initgroups failure, so just check for + non-zero. */ + + if (igflag) + { + struct passwd *pw = getpwuid(uid); + if (!pw) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "cannot run initgroups(): " + "no passwd entry for uid=%ld", (long int)uid); + + if (initgroups(pw->pw_name, gid) != 0) + log_write(0,LOG_MAIN|LOG_PANIC_DIE,"initgroups failed for uid=%ld: %s", + (long int)uid, strerror(errno)); + } + + if (setgid(gid) < 0 || setuid(uid) < 0) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "unable to set gid=%ld or uid=%ld " + "(euid=%ld): %s", (long int)gid, (long int)uid, (long int)euid, msg); + } + +/* Debugging output included uid/gid and all groups */ + +DEBUG(D_uid) + { + int group_count, save_errno; + gid_t group_list[EXIM_GROUPLIST_SIZE]; + debug_printf("changed uid/gid: %s\n uid=%ld gid=%ld pid=%ld\n", msg, + (long int)geteuid(), (long int)getegid(), (long int)getpid()); + group_count = getgroups(nelem(group_list), group_list); + save_errno = errno; + debug_printf(" auxiliary group list:"); + if (group_count > 0) + for (int i = 0; i < group_count; i++) debug_printf(" %d", (int)group_list[i]); + else if (group_count < 0) + debug_printf(" ", strerror(save_errno)); + else debug_printf(" "); + debug_printf("\n"); + } +} + + + + +/************************************************* +* Exit point * +*************************************************/ + +/* Exim exits via this function so that it always clears up any open +databases. + +Arguments: + rc return code + +Returns: does not return +*/ + +void +exim_exit(int rc) +{ +search_tidyup(); +store_exit(); +DEBUG(D_any) + debug_printf(">>>>>>>>>>>>>>>> Exim pid=%d (%s) terminating with rc=%d " + ">>>>>>>>>>>>>>>>\n", + (int)getpid(), process_purpose, rc); +exit(rc); +} + + +void +exim_underbar_exit(int rc) +{ +store_exit(); +DEBUG(D_any) + debug_printf(">>>>>>>>>>>>>>>> Exim pid=%d (%s) terminating with rc=%d " + ">>>>>>>>>>>>>>>>\n", + (int)getpid(), process_purpose, rc); +_exit(rc); +} + + + +/* Print error string, then die */ +static void +exim_fail(const char * fmt, ...) +{ +va_list ap; +va_start(ap, fmt); +vfprintf(stderr, fmt, ap); +exit(EXIT_FAILURE); +} + +/* fail if a length is too long */ +static inline void +exim_len_fail_toolong(int itemlen, int maxlen, const char *description) +{ +if (itemlen <= maxlen) + return; +fprintf(stderr, "exim: length limit exceeded (%d > %d) for: %s\n", + itemlen, maxlen, description); +exit(EXIT_FAILURE); +} + +/* only pass through the string item back to the caller if it's short enough */ +static inline const uschar * +exim_str_fail_toolong(const uschar *item, int maxlen, const char *description) +{ +exim_len_fail_toolong(Ustrlen(item), maxlen, description); +return item; +} + +/* exim_chown_failure() called from exim_chown()/exim_fchown() on failure +of chown()/fchown(). See src/functions.h for more explanation */ +int +exim_chown_failure(int fd, const uschar *name, uid_t owner, gid_t group) +{ +int saved_errno = errno; /* from the preceeding chown call */ +#if 1 +log_write(0, LOG_MAIN|LOG_PANIC, + __FILE__ ":%d: chown(%s, %d:%d) failed (%s)." + " Please contact the authors and refer to https://bugs.exim.org/show_bug.cgi?id=2391", + __LINE__, name?name:US"", owner, group, strerror(errno)); +#else +/* I leave this here, commented, in case the "bug"(?) comes up again. + It is not an Exim bug, but we can provide a workaround. + See Bug 2391 + HS 2019-04-18 */ + +struct stat buf; + +if (0 == (fd < 0 ? stat(name, &buf) : fstat(fd, &buf))) +{ + if (buf.st_uid == owner && buf.st_gid == group) return 0; + log_write(0, LOG_MAIN|LOG_PANIC, "Wrong ownership on %s", name); +} +else log_write(0, LOG_MAIN|LOG_PANIC, "Stat failed on %s: %s", name, strerror(errno)); + +#endif +errno = saved_errno; +return -1; +} + + +/************************************************* +* Extract port from host address * +*************************************************/ + +/* Called to extract the port from the values given to -oMa and -oMi. +It also checks the syntax of the address, and terminates it before the +port data when a port is extracted. + +Argument: + address the address, with possible port on the end + +Returns: the port, or zero if there isn't one + bombs out on a syntax error +*/ + +static int +check_port(uschar *address) +{ +int port = host_address_extract_port(address); +if (string_is_ip_address(address, NULL) == 0) + exim_fail("exim abandoned: \"%s\" is not an IP address\n", address); +return port; +} + + + +/************************************************* +* Test/verify an address * +*************************************************/ + +/* This function is called by the -bv and -bt code. It extracts a working +address from a full RFC 822 address. This isn't really necessary per se, but it +has the effect of collapsing source routes. + +Arguments: + s the address string + flags flag bits for verify_address() + exit_value to be set for failures + +Returns: nothing +*/ + +static void +test_address(uschar *s, int flags, int *exit_value) +{ +int start, end, domain; +uschar *parse_error = NULL; +uschar *address = parse_extract_address(s, &parse_error, &start, &end, &domain, + FALSE); +if (!address) + { + fprintf(stdout, "syntax error: %s\n", parse_error); + *exit_value = 2; + } +else + { + int rc = verify_address(deliver_make_addr(address,TRUE), stdout, flags, -1, + -1, -1, NULL, NULL, NULL); + if (rc == FAIL) *exit_value = 2; + else if (rc == DEFER && *exit_value == 0) *exit_value = 1; + } +} + + + +/************************************************* +* Show supported features * +*************************************************/ + +static void +show_string(BOOL is_stdout, gstring * g) +{ +const uschar * s = string_from_gstring(g); +if (s) + if (is_stdout) fputs(CCS s, stdout); + else debug_printf("%s", s); +} + + +static gstring * +show_db_version(gstring * g) +{ +#ifdef DB_VERSION_STRING +DEBUG(D_any) + { + g = string_fmt_append(g, "Library version: BDB: Compile: %s\n", DB_VERSION_STRING); + g = string_fmt_append(g, " Runtime: %s\n", + db_version(NULL, NULL, NULL)); + } +else + g = string_fmt_append(g, "Berkeley DB: %s\n", DB_VERSION_STRING); + +#elif defined(BTREEVERSION) && defined(HASHVERSION) +# ifdef USE_DB + g = string_cat(g, US"Probably Berkeley DB version 1.8x (native mode)\n"); +# else + g = string_cat(g, US"Probably Berkeley DB version 1.8x (compatibility mode)\n"); +# endif + +#elif defined(_DBM_RDONLY) || defined(dbm_dirfno) +g = string_cat(g, US"Probably ndbm\n"); +#elif defined(USE_TDB) +g = string_cat(g, US"Using tdb\n"); +#else +# ifdef USE_GDBM + g = string_cat(g, US"Probably GDBM (native mode)\n"); +# else + g = string_cat(g, US"Probably GDBM (compatibility mode)\n"); +# endif +#endif +return g; +} + + +/* This function is called for -bV/--version and for -d to output the optional +features of the current Exim binary. + +Arguments: BOOL, true for stdout else debug channel +Returns: nothing +*/ + +static void +show_whats_supported(BOOL is_stdout) +{ +rmark reset_point = store_mark(); +gstring * g = NULL; + +DEBUG(D_any) {} else g = show_db_version(g); + +g = string_cat(g, US"Support for:"); +#ifdef SUPPORT_CRYPTEQ + g = string_cat(g, US" crypteq"); +#endif +#if HAVE_ICONV + g = string_cat(g, US" iconv()"); +#endif +#if HAVE_IPV6 + g = string_cat(g, US" IPv6"); +#endif +#ifdef HAVE_SETCLASSRESOURCES + g = string_cat(g, US" use_setclassresources"); +#endif +#ifdef SUPPORT_PAM + g = string_cat(g, US" PAM"); +#endif +#ifdef EXIM_PERL + g = string_cat(g, US" Perl"); +#endif +#ifdef EXPAND_DLFUNC + g = string_cat(g, US" Expand_dlfunc"); +#endif +#ifdef USE_TCP_WRAPPERS + g = string_cat(g, US" TCPwrappers"); +#endif +#ifdef USE_GNUTLS + g = string_cat(g, US" GnuTLS"); +#endif +#ifdef USE_OPENSSL + g = string_cat(g, US" OpenSSL"); +#endif +#ifndef DISABLE_TLS_RESUME + g = string_cat(g, US" TLS_resume"); +#endif +#ifdef SUPPORT_TRANSLATE_IP_ADDRESS + g = string_cat(g, US" translate_ip_address"); +#endif +#ifdef SUPPORT_MOVE_FROZEN_MESSAGES + g = string_cat(g, US" move_frozen_messages"); +#endif +#ifdef WITH_CONTENT_SCAN + g = string_cat(g, US" Content_Scanning"); +#endif +#ifdef SUPPORT_DANE + g = string_cat(g, US" DANE"); +#endif +#ifndef DISABLE_DKIM + g = string_cat(g, US" DKIM"); +#endif +#ifdef SUPPORT_DMARC + g = string_cat(g, US" DMARC"); +#endif +#ifndef DISABLE_DNSSEC + g = string_cat(g, US" DNSSEC"); +#endif +#ifndef DISABLE_EVENT + g = string_cat(g, US" Event"); +#endif +#ifdef SUPPORT_I18N + g = string_cat(g, US" I18N"); +#endif +#ifndef DISABLE_OCSP + g = string_cat(g, US" OCSP"); +#endif +#ifndef DISABLE_PIPE_CONNECT + g = string_cat(g, US" PIPECONNECT"); +#endif +#ifndef DISABLE_PRDR + g = string_cat(g, US" PRDR"); +#endif +#ifdef SUPPORT_PROXY + g = string_cat(g, US" PROXY"); +#endif +#ifndef DISABLE_QUEUE_RAMP + g = string_cat(g, US" Queue_Ramp"); +#endif +#ifdef SUPPORT_SOCKS + g = string_cat(g, US" SOCKS"); +#endif +#ifdef SUPPORT_SPF + g = string_cat(g, US" SPF"); +#endif +#if defined(SUPPORT_SRS) + g = string_cat(g, US" SRS"); +#endif +#ifdef TCP_FASTOPEN + tcp_init(); + if (f.tcp_fastopen_ok) g = string_cat(g, US" TCP_Fast_Open"); +#endif +#ifdef EXPERIMENTAL_ARC + g = string_cat(g, US" Experimental_ARC"); +#endif +#ifdef EXPERIMENTAL_BRIGHTMAIL + g = string_cat(g, US" Experimental_Brightmail"); +#endif +#ifdef EXPERIMENTAL_DCC + g = string_cat(g, US" Experimental_DCC"); +#endif +#ifdef EXPERIMENTAL_DSN_INFO + g = string_cat(g, US" Experimental_DSN_info"); +#endif +#ifdef EXPERIMENTAL_ESMTP_LIMITS + g = string_cat(g, US" Experimental_ESMTP_Limits"); +#endif +#ifdef EXPERIMENTAL_QUEUEFILE + g = string_cat(g, US" Experimental_QUEUEFILE"); +#endif +g = string_cat(g, US"\n"); + +g = string_cat(g, US"Lookups (built-in):"); +#if defined(LOOKUP_LSEARCH) && LOOKUP_LSEARCH!=2 + g = string_cat(g, US" lsearch wildlsearch nwildlsearch iplsearch"); +#endif +#if defined(LOOKUP_CDB) && LOOKUP_CDB!=2 + g = string_cat(g, US" cdb"); +#endif +#if defined(LOOKUP_DBM) && LOOKUP_DBM!=2 + g = string_cat(g, US" dbm dbmjz dbmnz"); +#endif +#if defined(LOOKUP_DNSDB) && LOOKUP_DNSDB!=2 + g = string_cat(g, US" dnsdb"); +#endif +#if defined(LOOKUP_DSEARCH) && LOOKUP_DSEARCH!=2 + g = string_cat(g, US" dsearch"); +#endif +#if defined(LOOKUP_IBASE) && LOOKUP_IBASE!=2 + g = string_cat(g, US" ibase"); +#endif +#if defined(LOOKUP_JSON) && LOOKUP_JSON!=2 + g = string_cat(g, US" json"); +#endif +#if defined(LOOKUP_LDAP) && LOOKUP_LDAP!=2 + g = string_cat(g, US" ldap ldapdn ldapm"); +#endif +#ifdef LOOKUP_LMDB + g = string_cat(g, US" lmdb"); +#endif +#if defined(LOOKUP_MYSQL) && LOOKUP_MYSQL!=2 + g = string_cat(g, US" mysql"); +#endif +#if defined(LOOKUP_NIS) && LOOKUP_NIS!=2 + g = string_cat(g, US" nis nis0"); +#endif +#if defined(LOOKUP_NISPLUS) && LOOKUP_NISPLUS!=2 + g = string_cat(g, US" nisplus"); +#endif +#if defined(LOOKUP_ORACLE) && LOOKUP_ORACLE!=2 + g = string_cat(g, US" oracle"); +#endif +#if defined(LOOKUP_PASSWD) && LOOKUP_PASSWD!=2 + g = string_cat(g, US" passwd"); +#endif +#if defined(LOOKUP_PGSQL) && LOOKUP_PGSQL!=2 + g = string_cat(g, US" pgsql"); +#endif +#if defined(LOOKUP_REDIS) && LOOKUP_REDIS!=2 + g = string_cat(g, US" redis"); +#endif +#if defined(LOOKUP_SQLITE) && LOOKUP_SQLITE!=2 + g = string_cat(g, US" sqlite"); +#endif +#if defined(LOOKUP_TESTDB) && LOOKUP_TESTDB!=2 + g = string_cat(g, US" testdb"); +#endif +#if defined(LOOKUP_WHOSON) && LOOKUP_WHOSON!=2 + g = string_cat(g, US" whoson"); +#endif +g = string_cat(g, US"\n"); + +g = auth_show_supported(g); +g = route_show_supported(g); +g = transport_show_supported(g); + +#ifdef WITH_CONTENT_SCAN +g = malware_show_supported(g); +#endif +show_string(is_stdout, g); g = NULL; + +if (fixed_never_users[0] > 0) + { + int i; + g = string_cat(g, US"Fixed never_users: "); + for (i = 1; i <= (int)fixed_never_users[0] - 1; i++) + string_fmt_append(g, "%u:", (unsigned)fixed_never_users[i]); + g = string_fmt_append(g, "%u\n", (unsigned)fixed_never_users[i]); + } + +g = string_fmt_append(g, "Configure owner: %d:%d\n", config_uid, config_gid); + +g = string_fmt_append(g, "Size of off_t: " SIZE_T_FMT "\n", sizeof(off_t)); + +/* Everything else is details which are only worth reporting when debugging. +Perhaps the tls_version_report should move into this too. */ +DEBUG(D_any) + { + +/* clang defines __GNUC__ (at least, for me) so test for it first */ +#if defined(__clang__) + g = string_fmt_append(g, "Compiler: CLang [%s]\n", __clang_version__); +#elif defined(__GNUC__) + g = string_fmt_append(g, "Compiler: GCC [%s]\n", +# ifdef __VERSION__ + __VERSION__ +# else + "? unknown version ?" +# endif + ); +#else + g = string_cat(g, US"Compiler: \n"); +#endif + +#if defined(__GLIBC__) && !defined(__UCLIBC__) + g = string_fmt_append(g, "Library version: Glibc: Compile: %d.%d\n", + __GLIBC__, __GLIBC_MINOR__); + if (__GLIBC_PREREQ(2, 1)) + g = string_fmt_append(g, " Runtime: %s\n", + gnu_get_libc_version()); +#endif + +g = show_db_version(g); + +#ifndef DISABLE_TLS + g = tls_version_report(g); +#endif +#ifdef SUPPORT_I18N + g = utf8_version_report(g); +#endif +#ifdef SUPPORT_DMARC + g = dmarc_version_report(g); +#endif +#ifdef SUPPORT_SPF + g = spf_lib_version_report(g); +#endif + +show_string(is_stdout, g); +g = NULL; + +for (auth_info * authi = auths_available; *authi->driver_name != '\0'; ++authi) + if (authi->version_report) + g = (*authi->version_report)(g); + + /* PCRE_PRERELEASE is either defined and empty or a bare sequence of + characters; unless it's an ancient version of PCRE in which case it + is not defined. */ +#ifndef PCRE_PRERELEASE +# define PCRE_PRERELEASE +#endif +#define QUOTE(X) #X +#define EXPAND_AND_QUOTE(X) QUOTE(X) + { + uschar buf[24]; + pcre2_config(PCRE2_CONFIG_VERSION, buf); + g = string_fmt_append(g, "Library version: PCRE2: Compile: %d.%d%s\n" + " Runtime: %s\n", + PCRE2_MAJOR, PCRE2_MINOR, + EXPAND_AND_QUOTE(PCRE2_PRERELEASE) "", + buf); + } +#undef QUOTE +#undef EXPAND_AND_QUOTE + +show_string(is_stdout, g); +g = NULL; + +init_lookup_list(); +for (int i = 0; i < lookup_list_count; i++) + if (lookup_list[i]->version_report) + g = lookup_list[i]->version_report(g); +show_string(is_stdout, g); +g = NULL; + +#ifdef WHITELIST_D_MACROS + g = string_fmt_append(g, "WHITELIST_D_MACROS: \"%s\"\n", WHITELIST_D_MACROS); +#else + g = string_cat(g, US"WHITELIST_D_MACROS unset\n"); +#endif +#ifdef TRUSTED_CONFIG_LIST + g = string_fmt_append(g, "TRUSTED_CONFIG_LIST: \"%s\"\n", TRUSTED_CONFIG_LIST); +#else + g = string_cat(g, US"TRUSTED_CONFIG_LIST unset\n"); +#endif + } + +show_string(is_stdout, g); +store_reset(reset_point); +} + + +/************************************************* +* Show auxiliary information about Exim * +*************************************************/ + +static void +show_exim_information(enum commandline_info request, FILE *stream) +{ +switch(request) + { + case CMDINFO_NONE: + fprintf(stream, "Oops, something went wrong.\n"); + return; + case CMDINFO_HELP: + fprintf(stream, +"The -bI: flag takes a string indicating which information to provide.\n" +"If the string is not recognised, you'll get this help (on stderr).\n" +"\n" +" exim -bI:help this information\n" +" exim -bI:dscp list of known dscp value keywords\n" +" exim -bI:sieve list of supported sieve extensions\n" +); + return; + case CMDINFO_SIEVE: + for (const uschar ** pp = exim_sieve_extension_list; *pp; ++pp) + fprintf(stream, "%s\n", *pp); + return; + case CMDINFO_DSCP: + dscp_list_to_stream(stream); + return; + } +} + + +/************************************************* +* Quote a local part * +*************************************************/ + +/* This function is used when a sender address or a From: or Sender: header +line is being created from the caller's login, or from an authenticated_id. It +applies appropriate quoting rules for a local part. + +Argument: the local part +Returns: the local part, quoted if necessary +*/ + +uschar * +local_part_quote(uschar *lpart) +{ +BOOL needs_quote = FALSE; +gstring * g; + +for (uschar * t = lpart; !needs_quote && *t != 0; t++) + { + needs_quote = !isalnum(*t) && strchr("!#$%&'*+-/=?^_`{|}~", *t) == NULL && + (*t != '.' || t == lpart || t[1] == 0); + } + +if (!needs_quote) return lpart; + +g = string_catn(NULL, US"\"", 1); + +for (;;) + { + uschar *nq = US Ustrpbrk(lpart, "\\\""); + if (nq == NULL) + { + g = string_cat(g, lpart); + break; + } + g = string_catn(g, lpart, nq - lpart); + g = string_catn(g, US"\\", 1); + g = string_catn(g, nq, 1); + lpart = nq + 1; + } + +g = string_catn(g, US"\"", 1); +return string_from_gstring(g); +} + + + +#ifdef USE_READLINE +/************************************************* +* Load readline() functions * +*************************************************/ + +/* This function is called from testing executions that read data from stdin, +but only when running as the calling user. Currently, only -be does this. The +function loads the readline() function library and passes back the functions. +On some systems, it needs the curses library, so load that too, but try without +it if loading fails. All this functionality has to be requested at build time. + +Arguments: + fn_readline_ptr pointer to where to put the readline pointer + fn_addhist_ptr pointer to where to put the addhistory function + +Returns: the dlopen handle or NULL on failure +*/ + +static void * +set_readline(char * (**fn_readline_ptr)(const char *), + void (**fn_addhist_ptr)(const char *)) +{ +void *dlhandle; +void *dlhandle_curses = dlopen("libcurses." DYNLIB_FN_EXT, RTLD_GLOBAL|RTLD_LAZY); + +dlhandle = dlopen("libreadline." DYNLIB_FN_EXT, RTLD_GLOBAL|RTLD_NOW); +if (dlhandle_curses) dlclose(dlhandle_curses); + +if (dlhandle) + { + /* Checked manual pages; at least in GNU Readline 6.1, the prototypes are: + * char * readline (const char *prompt); + * void add_history (const char *string); + */ + *fn_readline_ptr = (char *(*)(const char*))dlsym(dlhandle, "readline"); + *fn_addhist_ptr = (void(*)(const char*))dlsym(dlhandle, "add_history"); + } +else + DEBUG(D_any) debug_printf("failed to load readline: %s\n", dlerror()); + +return dlhandle; +} +#endif + + + +/************************************************* +* Get a line from stdin for testing things * +*************************************************/ + +/* This function is called when running tests that can take a number of lines +of input (for example, -be and -bt). It handles continuations and trailing +spaces. And prompting and a blank line output on eof. If readline() is in use, +the arguments are non-NULL and provide the relevant functions. + +Arguments: + fn_readline readline function or NULL + fn_addhist addhist function or NULL + +Returns: pointer to dynamic memory, or NULL at end of file +*/ + +static uschar * +get_stdinput(char *(*fn_readline)(const char *), void(*fn_addhist)(const char *)) +{ +gstring * g = NULL; +BOOL had_input = FALSE; + +if (!fn_readline) { printf("> "); fflush(stdout); } + +for (int i = 0;; i++) + { + uschar buffer[1024]; + uschar * p, * ss; + +#ifdef USE_READLINE + char *readline_line = NULL; + if (fn_readline) + { + if (!(readline_line = fn_readline((i > 0)? "":"> "))) break; + if (*readline_line && fn_addhist) fn_addhist(readline_line); + p = US readline_line; + } + else +#endif + + /* readline() not in use */ + + { + if (Ufgets(buffer, sizeof(buffer), stdin) == NULL) break; /*EOF*/ + p = buffer; + } + + /* Handle the line */ + + had_input = TRUE; + ss = p + Ustrlen(p); + while (ss > p && isspace(ss[-1])) ss--; /* strip trailing newline (and spaces) */ + + if (i > 0) + while (p < ss && isspace(*p)) p++; /* strip leading space after cont */ + + g = string_catn(g, p, ss - p); + +#ifdef USE_READLINE + if (fn_readline) free(readline_line); +#endif + + /* g can only be NULL if ss==p */ + if (ss == p || g->s[g->ptr-1] != '\\') /* not continuation; done */ + break; + + --g->ptr; /* drop the \ */ + } + +if (had_input) return g ? string_from_gstring(g) : US""; +printf("\n"); +return NULL; +} + + + +/************************************************* +* Output usage information for the program * +*************************************************/ + +/* This function is called when there are no recipients + or a specific --help argument was added. + +Arguments: + progname information on what name we were called by + +Returns: DOES NOT RETURN +*/ + +static void +exim_usage(uschar *progname) +{ + +/* Handle specific program invocation variants */ +if (Ustrcmp(progname, US"-mailq") == 0) + exim_fail( + "mailq - list the contents of the mail queue\n\n" + "For a list of options, see the Exim documentation.\n"); + +/* Generic usage - we output this whatever happens */ +exim_fail( + "Exim is a Mail Transfer Agent. It is normally called by Mail User Agents,\n" + "not directly from a shell command line. Options and/or arguments control\n" + "what it does when called. For a list of options, see the Exim documentation.\n"); +} + + + +/************************************************* +* Validate that the macros given are okay * +*************************************************/ + +/* Typically, Exim will drop privileges if macros are supplied. In some +cases, we want to not do so. + +Arguments: opt_D_used - true if the commandline had a "-D" option +Returns: true if trusted, false otherwise +*/ + +static BOOL +macros_trusted(BOOL opt_D_used) +{ +#ifdef WHITELIST_D_MACROS +uschar *whitelisted, *end, *p, **whites; +int white_count, i, n; +size_t len; +BOOL prev_char_item, found; +#endif + +if (!opt_D_used) + return TRUE; +#ifndef WHITELIST_D_MACROS +return FALSE; +#else + +/* We only trust -D overrides for some invoking users: +root, the exim run-time user, the optional config owner user. +I don't know why config-owner would be needed, but since they can own the +config files anyway, there's no security risk to letting them override -D. */ +if ( ! ((real_uid == root_uid) + || (real_uid == exim_uid) +#ifdef CONFIGURE_OWNER + || (real_uid == config_uid) +#endif + )) + { + debug_printf("macros_trusted rejecting macros for uid %d\n", (int) real_uid); + return FALSE; + } + +/* Get a list of macros which are whitelisted */ +whitelisted = string_copy_perm(US WHITELIST_D_MACROS, FALSE); +prev_char_item = FALSE; +white_count = 0; +for (p = whitelisted; *p != '\0'; ++p) + { + if (*p == ':' || isspace(*p)) + { + *p = '\0'; + if (prev_char_item) + ++white_count; + prev_char_item = FALSE; + continue; + } + if (!prev_char_item) + prev_char_item = TRUE; + } +end = p; +if (prev_char_item) + ++white_count; +if (!white_count) + return FALSE; +whites = store_malloc(sizeof(uschar *) * (white_count+1)); +for (p = whitelisted, i = 0; (p != end) && (i < white_count); ++p) + { + if (*p != '\0') + { + whites[i++] = p; + if (i == white_count) + break; + while (*p != '\0' && p < end) + ++p; + } + } +whites[i] = NULL; + +/* The list of commandline macros should be very short. +Accept the N*M complexity. */ +for (macro_item * m = macros_user; m; m = m->next) if (m->command_line) + { + found = FALSE; + for (uschar ** w = whites; *w; ++w) + if (Ustrcmp(*w, m->name) == 0) + { + found = TRUE; + break; + } + if (!found) + return FALSE; + if (!m->replacement) + continue; + if ((len = m->replen) == 0) + continue; + if (!regex_match(regex_whitelisted_macro, m->replacement, len, NULL)) + return FALSE; + } +DEBUG(D_any) debug_printf("macros_trusted overridden to true by whitelisting\n"); +return TRUE; +#endif +} + + +/************************************************* +* Expansion testing * +*************************************************/ + +/* Expand and print one item, doing macro-processing. + +Arguments: + item line for expansion +*/ + +static void +expansion_test_line(const uschar * line) +{ +int len; +BOOL dummy_macexp; +uschar * s; + +Ustrncpy(big_buffer, line, big_buffer_size); +big_buffer[big_buffer_size-1] = '\0'; +len = Ustrlen(big_buffer); + +(void) macros_expand(0, &len, &dummy_macexp); + +if (isupper(big_buffer[0])) + { + if (macro_read_assignment(big_buffer)) + printf("Defined macro '%s'\n", mlast->name); + } +else + if ((s = expand_string(big_buffer))) printf("%s\n", CS s); + else printf("Failed: %s\n", expand_string_message); +} + + + +/************************************************* +* Entry point and high-level code * +*************************************************/ + +/* Entry point for the Exim mailer. Analyse the arguments and arrange to take +the appropriate action. All the necessary functions are present in the one +binary. I originally thought one should split it up, but it turns out that so +much of the apparatus is needed in each chunk that one might as well just have +it all available all the time, which then makes the coding easier as well. + +Arguments: + argc count of entries in argv + argv argument strings, with argv[0] being the program name + +Returns: EXIT_SUCCESS if terminated successfully + EXIT_FAILURE otherwise, except when a message has been sent + to the sender, and -oee was given +*/ + +int +main(int argc, char **cargv) +{ +uschar **argv = USS cargv; +int arg_receive_timeout = -1; +int arg_smtp_receive_timeout = -1; +int arg_error_handling = error_handling; +int filter_sfd = -1; +int filter_ufd = -1; +int group_count; +int i, rv; +int list_queue_option = 0; +int msg_action = 0; +int msg_action_arg = -1; +int namelen = argv[0] ? Ustrlen(argv[0]) : 0; +int queue_only_reason = 0; +#ifdef EXIM_PERL +int perl_start_option = 0; +#endif +int recipients_arg = argc; +int sender_address_domain = 0; +int test_retry_arg = -1; +int test_rewrite_arg = -1; +gid_t original_egid; +BOOL arg_queue_only = FALSE; +BOOL bi_option = FALSE; +BOOL checking = FALSE; +BOOL count_queue = FALSE; +BOOL expansion_test = FALSE; +BOOL extract_recipients = FALSE; +BOOL flag_G = FALSE; +BOOL flag_n = FALSE; +BOOL forced_delivery = FALSE; +BOOL f_end_dot = FALSE; +BOOL deliver_give_up = FALSE; +BOOL list_queue = FALSE; +BOOL list_options = FALSE; +BOOL list_config = FALSE; +BOOL local_queue_only; +BOOL one_msg_action = FALSE; +BOOL opt_D_used = FALSE; +BOOL queue_only_set = FALSE; +BOOL receiving_message = TRUE; +BOOL sender_ident_set = FALSE; +BOOL session_local_queue_only; +BOOL unprivileged; +BOOL removed_privilege = FALSE; +BOOL usage_wanted = FALSE; +BOOL verify_address_mode = FALSE; +BOOL verify_as_sender = FALSE; +BOOL rcpt_verify_quota = FALSE; +BOOL version_printed = FALSE; +uschar *alias_arg = NULL; +uschar *called_as = US""; +uschar *cmdline_syslog_name = NULL; +uschar *start_queue_run_id = NULL; +uschar *stop_queue_run_id = NULL; +uschar *expansion_test_message = NULL; +const uschar *ftest_domain = NULL; +const uschar *ftest_localpart = NULL; +const uschar *ftest_prefix = NULL; +const uschar *ftest_suffix = NULL; +uschar *log_oneline = NULL; +uschar *malware_test_file = NULL; +uschar *real_sender_address; +uschar *originator_home = US"/"; +size_t sz; + +struct passwd *pw; +struct stat statbuf; +pid_t passed_qr_pid = (pid_t)0; +int passed_qr_pipe = -1; +gid_t group_list[EXIM_GROUPLIST_SIZE]; + +/* For the -bI: flag */ +enum commandline_info info_flag = CMDINFO_NONE; +BOOL info_stdout = FALSE; + +/* Possible options for -R and -S */ + +static uschar *rsopts[] = { US"f", US"ff", US"r", US"rf", US"rff" }; + +/* Need to define this in case we need to change the environment in order +to get rid of a bogus time zone. We have to make it char rather than uschar +because some OS define it in /usr/include/unistd.h. */ + +extern char **environ; + +#ifdef MEASURE_TIMING +(void)gettimeofday(×tamp_startup, NULL); +#endif + +store_init(); /* Initialise the memory allocation susbsystem */ +pcre_init(); /* Set up memory handling for pcre */ + +/* If the Exim user and/or group and/or the configuration file owner/group were +defined by ref:name at build time, we must now find the actual uid/gid values. +This is a feature to make the lives of binary distributors easier. */ + +#ifdef EXIM_USERNAME +if (route_finduser(US EXIM_USERNAME, &pw, &exim_uid)) + { + if (exim_uid == 0) + exim_fail("exim: refusing to run with uid 0 for \"%s\"\n", EXIM_USERNAME); + + /* If ref:name uses a number as the name, route_finduser() returns + TRUE with exim_uid set and pw coerced to NULL. */ + if (pw) + exim_gid = pw->pw_gid; +#ifndef EXIM_GROUPNAME + else + exim_fail( + "exim: ref:name should specify a usercode, not a group.\n" + "exim: can't let you get away with it unless you also specify a group.\n"); +#endif + } +else + exim_fail("exim: failed to find uid for user name \"%s\"\n", EXIM_USERNAME); +#endif + +#ifdef EXIM_GROUPNAME +if (!route_findgroup(US EXIM_GROUPNAME, &exim_gid)) + exim_fail("exim: failed to find gid for group name \"%s\"\n", EXIM_GROUPNAME); +#endif + +#ifdef CONFIGURE_OWNERNAME +if (!route_finduser(US CONFIGURE_OWNERNAME, NULL, &config_uid)) + exim_fail("exim: failed to find uid for user name \"%s\"\n", + CONFIGURE_OWNERNAME); +#endif + +/* We default the system_filter_user to be the Exim run-time user, as a +sane non-root value. */ +system_filter_uid = exim_uid; + +#ifdef CONFIGURE_GROUPNAME +if (!route_findgroup(US CONFIGURE_GROUPNAME, &config_gid)) + exim_fail("exim: failed to find gid for group name \"%s\"\n", + CONFIGURE_GROUPNAME); +#endif + +/* In the Cygwin environment, some initialization used to need doing. +It was fudged in by means of this macro; now no longer but we'll leave +it in case of others. */ + +#ifdef OS_INIT +OS_INIT +#endif + +/* Check a field which is patched when we are running Exim within its +testing harness; do a fast initial check, and then the whole thing. */ + +f.running_in_test_harness = + *running_status == '<' && Ustrcmp(running_status, "<<>>") == 0; +if (f.running_in_test_harness) + debug_store = TRUE; + +/* Protect against abusive argv[0] */ +if (!argv[0] || !argc) exim_fail("exim: executable name required\n"); +exim_str_fail_toolong(argv[0], PATH_MAX, "argv[0]"); + +/* The C standard says that the equivalent of setlocale(LC_ALL, "C") is obeyed +at the start of a program; however, it seems that some environments do not +follow this. A "strange" locale can affect the formatting of timestamps, so we +make quite sure. */ + +setlocale(LC_ALL, "C"); + +/* Get the offset between CLOCK_MONOTONIC/CLOCK_BOOTTIME and wallclock */ + +#ifdef _POSIX_MONOTONIC_CLOCK +exim_clock_init(); +#endif + +/* Set up the default handler for timing using alarm(). */ + +os_non_restarting_signal(SIGALRM, sigalrm_handler); + +/* Ensure we have a buffer for constructing log entries. Use malloc directly, +because store_malloc writes a log entry on failure. */ + +if (!(log_buffer = US malloc(LOG_BUFFER_SIZE))) + exim_fail("exim: failed to get store for log buffer\n"); + +/* Initialize the default log options. */ + +bits_set(log_selector, log_selector_size, log_default); + +/* Set log_stderr to stderr, provided that stderr exists. This gets reset to +NULL when the daemon is run and the file is closed. We have to use this +indirection, because some systems don't allow writing to the variable "stderr". +*/ + +if (fstat(fileno(stderr), &statbuf) >= 0) log_stderr = stderr; + +/* Ensure there is a big buffer for temporary use in several places. It is put +in malloc store so that it can be freed for enlargement if necessary. */ + +big_buffer = store_malloc(big_buffer_size); + +/* Set up the handler for the data request signal, and set the initial +descriptive text. */ + +process_info = store_get(PROCESS_INFO_SIZE, GET_TAINTED); +set_process_info("initializing"); +os_restarting_signal(SIGUSR1, usr1_handler); /* exiwhat */ +#ifdef SA_SIGINFO + { + struct sigaction act = { .sa_sigaction = segv_handler, .sa_flags = SA_RESETHAND | SA_SIGINFO }; + sigaction(SIGSEGV, &act, NULL); + } +#else +signal(SIGSEGV, segv_handler); /* log faults */ +#endif + +/* If running in a dockerized environment, the TERM signal is only +delegated to the PID 1 if we request it by setting an signal handler */ +if (getpid() == 1) signal(SIGTERM, term_handler); + +/* SIGHUP is used to get the daemon to reconfigure. It gets set as appropriate +in the daemon code. For the rest of Exim's uses, we ignore it. */ + +signal(SIGHUP, SIG_IGN); + +/* We don't want to die on pipe errors as the code is written to handle +the write error instead. */ + +signal(SIGPIPE, SIG_IGN); + +/* Under some circumstance on some OS, Exim can get called with SIGCHLD +set to SIG_IGN. This causes subprocesses that complete before the parent +process waits for them not to hang around, so when Exim calls wait(), nothing +is there. The wait() code has been made robust against this, but let's ensure +that SIGCHLD is set to SIG_DFL, because it's tidier to wait and get a process +ending status. We use sigaction rather than plain signal() on those OS where +SA_NOCLDWAIT exists, because we want to be sure it is turned off. (There was a +problem on AIX with this.) */ + +#ifdef SA_NOCLDWAIT + { + struct sigaction act; + act.sa_handler = SIG_DFL; + sigemptyset(&(act.sa_mask)); + act.sa_flags = 0; + sigaction(SIGCHLD, &act, NULL); + } +#else +signal(SIGCHLD, SIG_DFL); +#endif + +/* Save the arguments for use if we re-exec exim as a daemon after receiving +SIGHUP. */ + +sighup_argv = argv; + +/* Set up the version number. Set up the leading 'E' for the external form of +message ids, set the pointer to the internal form, and initialize it to +indicate no message being processed. */ + +version_init(); +message_id_option[0] = '-'; +message_id_external = message_id_option + 1; +message_id_external[0] = 'E'; +message_id = message_id_external + 1; +message_id[0] = 0; + +/* Set the umask to zero so that any files Exim creates using open() are +created with the modes that it specifies. NOTE: Files created with fopen() have +a problem, which was not recognized till rather late (February 2006). With this +umask, such files will be world writeable. (They are all content scanning files +in the spool directory, which isn't world-accessible, so this is not a +disaster, but it's untidy.) I don't want to change this overall setting, +however, because it will interact badly with the open() calls. Instead, there's +now a function called modefopen() that fiddles with the umask while calling +fopen(). */ + +(void)umask(0); + +/* Precompile the regular expression for matching a message id. Keep this in +step with the code that generates ids in the accept.c module. We need to do +this here, because the -M options check their arguments for syntactic validity +using mac_ismsgid, which uses this. */ + +regex_ismsgid = + regex_must_compile(US"^(?:[^\\W_]{6}-){2}[^\\W_]{2}$", FALSE, TRUE); + +/* Precompile the regular expression that is used for matching an SMTP error +code, possibly extended, at the start of an error message. Note that the +terminating whitespace character is included. */ + +regex_smtp_code = + regex_must_compile(US"^\\d\\d\\d\\s(?:\\d\\.\\d\\d?\\d?\\.\\d\\d?\\d?\\s)?", + FALSE, TRUE); + +#ifdef WHITELIST_D_MACROS +/* Precompile the regular expression used to filter the content of macros +given to -D for permissibility. */ + +regex_whitelisted_macro = + regex_must_compile(US"^[A-Za-z0-9_/.-]*$", FALSE, TRUE); +#endif + +for (i = 0; i < REGEX_VARS; i++) regex_vars[i] = NULL; + +/* If the program is called as "mailq" treat it as equivalent to "exim -bp"; +this seems to be a generally accepted convention, since one finds symbolic +links called "mailq" in standard OS configurations. */ + +if ((namelen == 5 && Ustrcmp(argv[0], "mailq") == 0) || + (namelen > 5 && Ustrncmp(argv[0] + namelen - 6, "/mailq", 6) == 0)) + { + list_queue = TRUE; + receiving_message = FALSE; + called_as = US"-mailq"; + } + +/* If the program is called as "rmail" treat it as equivalent to +"exim -i -oee", thus allowing UUCP messages to be input using non-SMTP mode, +i.e. preventing a single dot on a line from terminating the message, and +returning with zero return code, even in cases of error (provided an error +message has been sent). */ + +if ((namelen == 5 && Ustrcmp(argv[0], "rmail") == 0) || + (namelen > 5 && Ustrncmp(argv[0] + namelen - 6, "/rmail", 6) == 0)) + { + f.dot_ends = FALSE; + called_as = US"-rmail"; + errors_sender_rc = EXIT_SUCCESS; + } + +/* If the program is called as "rsmtp" treat it as equivalent to "exim -bS"; +this is a smail convention. */ + +if ((namelen == 5 && Ustrcmp(argv[0], "rsmtp") == 0) || + (namelen > 5 && Ustrncmp(argv[0] + namelen - 6, "/rsmtp", 6) == 0)) + { + smtp_input = smtp_batched_input = TRUE; + called_as = US"-rsmtp"; + } + +/* If the program is called as "runq" treat it as equivalent to "exim -q"; +this is a smail convention. */ + +if ((namelen == 4 && Ustrcmp(argv[0], "runq") == 0) || + (namelen > 4 && Ustrncmp(argv[0] + namelen - 5, "/runq", 5) == 0)) + { + queue_interval = 0; + receiving_message = FALSE; + called_as = US"-runq"; + } + +/* If the program is called as "newaliases" treat it as equivalent to +"exim -bi"; this is a sendmail convention. */ + +if ((namelen == 10 && Ustrcmp(argv[0], "newaliases") == 0) || + (namelen > 10 && Ustrncmp(argv[0] + namelen - 11, "/newaliases", 11) == 0)) + { + bi_option = TRUE; + receiving_message = FALSE; + called_as = US"-newaliases"; + } + +/* Save the original effective uid for a couple of uses later. It should +normally be root, but in some esoteric environments it may not be. */ + +original_euid = geteuid(); +original_egid = getegid(); + +/* Get the real uid and gid. If the caller is root, force the effective uid/gid +to be the same as the real ones. This makes a difference only if Exim is setuid +(or setgid) to something other than root, which could be the case in some +special configurations. */ + +real_uid = getuid(); +real_gid = getgid(); + +if (real_uid == root_uid) + { + if ((rv = setgid(real_gid))) + exim_fail("exim: setgid(%ld) failed: %s\n", + (long int)real_gid, strerror(errno)); + if ((rv = setuid(real_uid))) + exim_fail("exim: setuid(%ld) failed: %s\n", + (long int)real_uid, strerror(errno)); + } + +/* If neither the original real uid nor the original euid was root, Exim is +running in an unprivileged state. */ + +unprivileged = (real_uid != root_uid && original_euid != root_uid); + +/* For most of the args-parsing we need to use permanent pool memory */ + { + int old_pool = store_pool; + store_pool = POOL_PERM; + +/* Scan the program's arguments. Some can be dealt with right away; others are +simply recorded for checking and handling afterwards. Do a high-level switch +on the second character (the one after '-'), to save some effort. */ + + for (i = 1; i < argc; i++) + { + BOOL badarg = FALSE; + uschar * arg = argv[i]; + uschar * argrest; + int switchchar; + + /* An argument not starting with '-' is the start of a recipients list; + break out of the options-scanning loop. */ + + if (arg[0] != '-') + { + recipients_arg = i; + break; + } + + /* An option consisting of -- terminates the options */ + + if (Ustrcmp(arg, "--") == 0) + { + recipients_arg = i + 1; + break; + } + + /* Handle flagged options */ + + switchchar = arg[1]; + argrest = arg+2; + + /* Make all -ex options synonymous with -oex arguments, since that + is assumed by various callers. Also make -qR options synonymous with -R + options, as that seems to be required as well. Allow for -qqR too, and + the same for -S options. */ + + if (Ustrncmp(arg+1, "oe", 2) == 0 || + Ustrncmp(arg+1, "qR", 2) == 0 || + Ustrncmp(arg+1, "qS", 2) == 0) + { + switchchar = arg[2]; + argrest++; + } + else if (Ustrncmp(arg+1, "qqR", 3) == 0 || Ustrncmp(arg+1, "qqS", 3) == 0) + { + switchchar = arg[3]; + argrest += 2; + f.queue_2stage = TRUE; + } + + /* Make -r synonymous with -f, since it is a documented alias */ + + else if (arg[1] == 'r') switchchar = 'f'; + + /* Make -ov synonymous with -v */ + + else if (Ustrcmp(arg, "-ov") == 0) + { + switchchar = 'v'; + argrest++; + } + + /* deal with --option_aliases */ + else if (switchchar == '-') + { + if (Ustrcmp(argrest, "help") == 0) + { + usage_wanted = TRUE; + break; + } + else if (Ustrcmp(argrest, "version") == 0) + { + switchchar = 'b'; + argrest = US"V"; + } + } + + /* High-level switch on active initial letter */ + + switch(switchchar) + { + + /* sendmail uses -Ac and -Am to control which .cf file is used; + we ignore them. */ + case 'A': + if (!*argrest) { badarg = TRUE; break; } + else + { + BOOL ignore = FALSE; + switch (*argrest) + { + case 'c': + case 'm': + if (*(argrest + 1) == '\0') + ignore = TRUE; + break; + } + if (!ignore) badarg = TRUE; + } + break; + + /* -Btype is a sendmail option for 7bit/8bit setting. Exim is 8-bit clean + so has no need of it. */ + + case 'B': + if (!*argrest) i++; /* Skip over the type */ + break; + + + case 'b': + { + receiving_message = FALSE; /* Reset TRUE for -bm, -bS, -bs below */ + + switch (*argrest++) + { + /* -bd: Run in daemon mode, awaiting SMTP connections. + -bdf: Ditto, but in the foreground. + */ + case 'd': + f.daemon_listen = TRUE; + if (*argrest == 'f') f.background_daemon = FALSE; + else if (*argrest) badarg = TRUE; + break; + + /* -be: Run in expansion test mode + -bem: Ditto, but read a message from a file first + */ + case 'e': + expansion_test = checking = TRUE; + if (*argrest == 'm') + { + if (++i >= argc) { badarg = TRUE; break; } + expansion_test_message = argv[i]; + argrest++; + } + if (*argrest) badarg = TRUE; + break; + + /* -bF: Run system filter test */ + case 'F': + filter_test |= checking = FTEST_SYSTEM; + if (*argrest) badarg = TRUE; + else if (++i < argc) filter_test_sfile = argv[i]; + else exim_fail("exim: file name expected after %s\n", argv[i-1]); + break; + + /* -bf: Run user filter test + -bfd: Set domain for filter testing + -bfl: Set local part for filter testing + -bfp: Set prefix for filter testing + -bfs: Set suffix for filter testing + */ + case 'f': + if (!*argrest) + { + filter_test |= checking = FTEST_USER; + if (++i < argc) filter_test_ufile = argv[i]; + else exim_fail("exim: file name expected after %s\n", argv[i-1]); + } + else + { + if (++i >= argc) + exim_fail("exim: string expected after %s\n", arg); + if (Ustrcmp(argrest, "d") == 0) ftest_domain = exim_str_fail_toolong(argv[i], EXIM_DOMAINNAME_MAX, "-bfd"); + else if (Ustrcmp(argrest, "l") == 0) ftest_localpart = exim_str_fail_toolong(argv[i], EXIM_LOCALPART_MAX, "-bfl"); + else if (Ustrcmp(argrest, "p") == 0) ftest_prefix = exim_str_fail_toolong(argv[i], EXIM_LOCALPART_MAX, "-bfp"); + else if (Ustrcmp(argrest, "s") == 0) ftest_suffix = exim_str_fail_toolong(argv[i], EXIM_LOCALPART_MAX, "-bfs"); + else badarg = TRUE; + } + break; + + /* -bh: Host checking - an IP address must follow. */ + case 'h': + if (!*argrest || Ustrcmp(argrest, "c") == 0) + { + if (++i >= argc) { badarg = TRUE; break; } + sender_host_address = string_copy_taint( + exim_str_fail_toolong(argv[i], EXIM_IPADDR_MAX, "-bh"), + GET_TAINTED); + host_checking = checking = f.log_testing_mode = TRUE; + f.host_checking_callout = *argrest == 'c'; + message_logs = FALSE; + } + else badarg = TRUE; + break; + + /* -bi: This option is used by sendmail to initialize *the* alias file, + though it has the -oA option to specify a different file. Exim has no + concept of *the* alias file, but since Sun's YP make script calls + sendmail this way, some support must be provided. */ + case 'i': + if (!*argrest) bi_option = TRUE; + else badarg = TRUE; + break; + + /* -bI: provide information, of the type to follow after a colon. + This is an Exim flag. */ + case 'I': + if (Ustrlen(argrest) >= 1 && *argrest == ':') + { + uschar *p = argrest+1; + info_flag = CMDINFO_HELP; + if (Ustrlen(p)) + if (strcmpic(p, CUS"sieve") == 0) + { + info_flag = CMDINFO_SIEVE; + info_stdout = TRUE; + } + else if (strcmpic(p, CUS"dscp") == 0) + { + info_flag = CMDINFO_DSCP; + info_stdout = TRUE; + } + else if (strcmpic(p, CUS"help") == 0) + info_stdout = TRUE; + } + else badarg = TRUE; + break; + + /* -bm: Accept and deliver message - the default option. Reinstate + receiving_message, which got turned off for all -b options. + -bmalware: test the filename given for malware */ + case 'm': + if (!*argrest) receiving_message = TRUE; + else if (Ustrcmp(argrest, "alware") == 0) + { + if (++i >= argc) { badarg = TRUE; break; } + checking = TRUE; + malware_test_file = argv[i]; + } + else badarg = TRUE; + break; + + /* -bnq: For locally originating messages, do not qualify unqualified + addresses. In the envelope, this causes errors; in header lines they + just get left. */ + case 'n': + if (Ustrcmp(argrest, "q") == 0) + { + f.allow_unqualified_sender = FALSE; + f.allow_unqualified_recipient = FALSE; + } + else badarg = TRUE; + break; + + /* -bpxx: List the contents of the mail queue, in various forms. If + the option is -bpc, just a queue count is needed. Otherwise, if the + first letter after p is r, then order is random. */ + case 'p': + if (*argrest == 'c') + { + count_queue = TRUE; + if (*++argrest) badarg = TRUE; + break; + } + + if (*argrest == 'r') + { + list_queue_option = 8; + argrest++; + } + else list_queue_option = 0; + + list_queue = TRUE; + + /* -bp: List the contents of the mail queue, top-level only */ + + if (!*argrest) {} + + /* -bpu: List the contents of the mail queue, top-level undelivered */ + + else if (Ustrcmp(argrest, "u") == 0) list_queue_option += 1; + + /* -bpa: List the contents of the mail queue, including all delivered */ + + else if (Ustrcmp(argrest, "a") == 0) list_queue_option += 2; + + /* Unknown after -bp[r] */ + + else badarg = TRUE; + break; + + + /* -bP: List the configuration variables given as the address list. + Force -v, so configuration errors get displayed. */ + case 'P': + + /* -bP config: we need to setup here, because later, + when list_options is checked, the config is read already */ + if (*argrest) + badarg = TRUE; + else if (argv[i+1] && Ustrcmp(argv[i+1], "config") == 0) + { + list_config = TRUE; + readconf_save_config(version_string); + } + else + { + list_options = TRUE; + debug_selector |= D_v; + debug_file = stderr; + } + break; + + /* -brt: Test retry configuration lookup */ + case 'r': + if (Ustrcmp(argrest, "t") == 0) + { + checking = TRUE; + test_retry_arg = i + 1; + goto END_ARG; + } + + /* -brw: Test rewrite configuration */ + + else if (Ustrcmp(argrest, "w") == 0) + { + checking = TRUE; + test_rewrite_arg = i + 1; + goto END_ARG; + } + else badarg = TRUE; + break; + + /* -bS: Read SMTP commands on standard input, but produce no replies - + all errors are reported by sending messages. */ + case 'S': + if (!*argrest) + smtp_input = smtp_batched_input = receiving_message = TRUE; + else badarg = TRUE; + break; + + /* -bs: Read SMTP commands on standard input and produce SMTP replies + on standard output. */ + case 's': + if (!*argrest) smtp_input = receiving_message = TRUE; + else badarg = TRUE; + break; + + /* -bt: address testing mode */ + case 't': + if (!*argrest) + f.address_test_mode = checking = f.log_testing_mode = TRUE; + else badarg = TRUE; + break; + + /* -bv: verify addresses */ + case 'v': + if (!*argrest) + verify_address_mode = checking = f.log_testing_mode = TRUE; + + /* -bvs: verify sender addresses */ + + else if (Ustrcmp(argrest, "s") == 0) + { + verify_address_mode = checking = f.log_testing_mode = TRUE; + verify_as_sender = TRUE; + } + else badarg = TRUE; + break; + + /* -bV: Print version string and support details */ + case 'V': + if (!*argrest) + { + printf("Exim version %s #%s built %s\n", version_string, + version_cnumber, version_date); + printf("%s\n", CS version_copyright); + version_printed = TRUE; + show_whats_supported(TRUE); + f.log_testing_mode = TRUE; + } + else badarg = TRUE; + break; + + /* -bw: inetd wait mode, accept a listening socket as stdin */ + case 'w': + f.inetd_wait_mode = TRUE; + f.background_daemon = FALSE; + f.daemon_listen = TRUE; + if (*argrest) + if ((inetd_wait_timeout = readconf_readtime(argrest, 0, FALSE)) <= 0) + exim_fail("exim: bad time value %s: abandoned\n", argv[i]); + break; + + default: + badarg = TRUE; + break; + } + break; + } + + + /* -C: change configuration file list; ignore if it isn't really + a change! Enforce a prefix check if required. */ + + case 'C': + if (!*argrest) + if (++i < argc) argrest = argv[i]; else { badarg = TRUE; break; } + if (Ustrcmp(config_main_filelist, argrest) != 0) + { + #ifdef ALT_CONFIG_PREFIX + int sep = 0; + int len = Ustrlen(ALT_CONFIG_PREFIX); + const uschar *list = argrest; + uschar *filename; + /* The argv is untainted, so big_buffer (also untainted) is ok to use */ + while((filename = string_nextinlist(&list, &sep, big_buffer, + big_buffer_size))) + if ( ( Ustrlen(filename) < len + || Ustrncmp(filename, ALT_CONFIG_PREFIX, len) != 0 + || Ustrstr(filename, "/../") != NULL + ) + && (Ustrcmp(filename, "/dev/null") != 0 || real_uid != root_uid) + ) + exim_fail("-C Permission denied\n"); + #endif + if (real_uid != root_uid) + { + #ifdef TRUSTED_CONFIG_LIST + + if (real_uid != exim_uid + #ifdef CONFIGURE_OWNER + && real_uid != config_uid + #endif + ) + f.trusted_config = FALSE; + else + { + FILE *trust_list = Ufopen(TRUSTED_CONFIG_LIST, "rb"); + if (trust_list) + { + struct stat statbuf; + + if (fstat(fileno(trust_list), &statbuf) != 0 || + (statbuf.st_uid != root_uid /* owner not root */ + #ifdef CONFIGURE_OWNER + && statbuf.st_uid != config_uid /* owner not the special one */ + #endif + ) || /* or */ + (statbuf.st_gid != root_gid /* group not root */ + #ifdef CONFIGURE_GROUP + && statbuf.st_gid != config_gid /* group not the special one */ + #endif + && (statbuf.st_mode & 020) != 0 /* group writeable */ + ) || /* or */ + (statbuf.st_mode & 2) != 0) /* world writeable */ + { + f.trusted_config = FALSE; + fclose(trust_list); + } + else + { + /* Well, the trust list at least is up to scratch... */ + rmark reset_point; + uschar *trusted_configs[32]; + int nr_configs = 0; + int i = 0; + int old_pool = store_pool; + store_pool = POOL_MAIN; + + reset_point = store_mark(); + while (Ufgets(big_buffer, big_buffer_size, trust_list)) + { + uschar *start = big_buffer, *nl; + while (*start && isspace(*start)) + start++; + if (*start != '/') + continue; + nl = Ustrchr(start, '\n'); + if (nl) + *nl = 0; + trusted_configs[nr_configs++] = string_copy(start); + if (nr_configs == nelem(trusted_configs)) + break; + } + fclose(trust_list); + + if (nr_configs) + { + int sep = 0; + const uschar *list = argrest; + uschar *filename; + while (f.trusted_config && (filename = string_nextinlist(&list, + &sep, big_buffer, big_buffer_size))) + { + for (i=0; i < nr_configs; i++) + if (Ustrcmp(filename, trusted_configs[i]) == 0) + break; + if (i == nr_configs) + { + f.trusted_config = FALSE; + break; + } + } + } + else /* No valid prefixes found in trust_list file. */ + f.trusted_config = FALSE; + store_reset(reset_point); + store_pool = old_pool; + } + } + else /* Could not open trust_list file. */ + f.trusted_config = FALSE; + } + #else + /* Not root; don't trust config */ + f.trusted_config = FALSE; + #endif + } + + config_main_filelist = argrest; + f.config_changed = TRUE; + } + break; + + + /* -D: set up a macro definition */ + + case 'D': +#ifdef DISABLE_D_OPTION + exim_fail("exim: -D is not available in this Exim binary\n"); +#else + { + int ptr = 0; + macro_item *m; + uschar name[24]; + uschar *s = argrest; + + opt_D_used = TRUE; + while (isspace(*s)) s++; + + if (*s < 'A' || *s > 'Z') + exim_fail("exim: macro name set by -D must start with " + "an upper case letter\n"); + + while (isalnum(*s) || *s == '_') + { + if (ptr < sizeof(name)-1) name[ptr++] = *s; + s++; + } + name[ptr] = 0; + if (ptr == 0) { badarg = TRUE; break; } + while (isspace(*s)) s++; + if (*s != 0) + { + if (*s++ != '=') { badarg = TRUE; break; } + while (isspace(*s)) s++; + } + + for (m = macros_user; m; m = m->next) + if (Ustrcmp(m->name, name) == 0) + exim_fail("exim: duplicated -D in command line\n"); + + m = macro_create(name, s, TRUE); + + if (clmacro_count >= MAX_CLMACROS) + exim_fail("exim: too many -D options on command line\n"); + clmacros[clmacro_count++] = + string_sprintf("-D%s=%s", m->name, m->replacement); + } + #endif + break; + + case 'd': + + /* -dropcr: Set this option. Now a no-op, retained for compatibility only. */ + + if (Ustrcmp(argrest, "ropcr") == 0) + { + /* drop_cr = TRUE; */ + } + + /* -dp: Set up a debug pretrigger buffer with given size. */ + + else if (Ustrcmp(argrest, "p") == 0) + if (++i >= argc) + badarg = TRUE; + else + debug_pretrigger_setup(argv[i]); + + /* -dt: Set a debug trigger selector */ + + else if (Ustrncmp(argrest, "t=", 2) == 0) + dtrigger_selector = (unsigned int) Ustrtol(argrest + 2, NULL, 0); + + /* -d: Set debug level (see also -v below). + If -dd is used, debugging subprocesses of the daemon is disabled. */ + + else + { + /* Use an intermediate variable so that we don't set debugging while + decoding the debugging bits. */ + + unsigned int selector = D_default; + debug_selector = 0; + debug_file = NULL; + if (*argrest == 'd') + { + f.debug_daemon = TRUE; + argrest++; + } + if (*argrest) + decode_bits(&selector, 1, debug_notall, argrest, + debug_options, debug_options_count, US"debug", 0); + debug_selector = selector; + } + break; + + + /* -E: This is a local error message. This option is not intended for + external use at all, but is not restricted to trusted callers because it + does no harm (just suppresses certain error messages) and if Exim is run + not setuid root it won't always be trusted when it generates error + messages using this option. If there is a message id following -E, point + message_reference at it, for logging. */ + + case 'E': + f.local_error_message = TRUE; + if (mac_ismsgid(argrest)) message_reference = argrest; + break; + + + /* -ex: The vacation program calls sendmail with the undocumented "-eq" + option, so it looks as if historically the -oex options are also callable + without the leading -o. So we have to accept them. Before the switch, + anything starting -oe has been converted to -e. Exim does not support all + of the sendmail error options. */ + + case 'e': + if (Ustrcmp(argrest, "e") == 0) + { + arg_error_handling = ERRORS_SENDER; + errors_sender_rc = EXIT_SUCCESS; + } + else if (Ustrcmp(argrest, "m") == 0) arg_error_handling = ERRORS_SENDER; + else if (Ustrcmp(argrest, "p") == 0) arg_error_handling = ERRORS_STDERR; + else if (Ustrcmp(argrest, "q") == 0) arg_error_handling = ERRORS_STDERR; + else if (Ustrcmp(argrest, "w") == 0) arg_error_handling = ERRORS_SENDER; + else badarg = TRUE; + break; + + + /* -F: Set sender's full name, used instead of the gecos entry from + the password file. Since users can usually alter their gecos entries, + there's no security involved in using this instead. The data can follow + the -F or be in the next argument. */ + + case 'F': + if (!*argrest) + if (++i < argc) argrest = argv[i]; else { badarg = TRUE; break; } + originator_name = string_copy_taint( + exim_str_fail_toolong(argrest, EXIM_HUMANNAME_MAX, "-F"), + GET_TAINTED); + f.sender_name_forced = TRUE; + break; + + + /* -f: Set sender's address - this value is only actually used if Exim is + run by a trusted user, or if untrusted_set_sender is set and matches the + address, except that the null address can always be set by any user. The + test for this happens later, when the value given here is ignored when not + permitted. For an untrusted user, the actual sender is still put in Sender: + if it doesn't match the From: header (unless no_local_from_check is set). + The data can follow the -f or be in the next argument. The -r switch is an + obsolete form of -f but since there appear to be programs out there that + use anything that sendmail has ever supported, better accept it - the + synonymizing is done before the switch above. + + At this stage, we must allow domain literal addresses, because we don't + know what the setting of allow_domain_literals is yet. Ditto for trailing + dots and strip_trailing_dot. */ + + case 'f': + { + int dummy_start, dummy_end; + uschar *errmess; + if (!*argrest) + if (i+1 < argc) argrest = argv[++i]; else { badarg = TRUE; break; } + (void) exim_str_fail_toolong(argrest, EXIM_DISPLAYMAIL_MAX, "-f"); + if (!*argrest) + *(sender_address = store_get(1, GET_UNTAINTED)) = '\0'; /* Ensure writeable memory */ + else + { + uschar * temp = argrest + Ustrlen(argrest) - 1; + while (temp >= argrest && isspace(*temp)) temp--; + if (temp >= argrest && *temp == '.') f_end_dot = TRUE; + allow_domain_literals = TRUE; + strip_trailing_dot = TRUE; +#ifdef SUPPORT_I18N + allow_utf8_domains = TRUE; +#endif + if (!(sender_address = parse_extract_address(argrest, &errmess, + &dummy_start, &dummy_end, &sender_address_domain, TRUE))) + exim_fail("exim: bad -f address \"%s\": %s\n", argrest, errmess); + + sender_address = string_copy_taint(sender_address, GET_TAINTED); +#ifdef SUPPORT_I18N + message_smtputf8 = string_is_utf8(sender_address); + allow_utf8_domains = FALSE; +#endif + allow_domain_literals = FALSE; + strip_trailing_dot = FALSE; + } + f.sender_address_forced = TRUE; + } + break; + + /* -G: sendmail invocation to specify that it's a gateway submission and + sendmail may complain about problems instead of fixing them. + We make it equivalent to an ACL "control = suppress_local_fixups" and do + not at this time complain about problems. */ + + case 'G': + flag_G = TRUE; + break; + + /* -h: Set the hop count for an incoming message. Exim does not currently + support this; it always computes it by counting the Received: headers. + To put it in will require a change to the spool header file format. */ + + case 'h': + if (!*argrest) + if (++i < argc) argrest = argv[i]; else { badarg = TRUE; break; } + if (!isdigit(*argrest)) badarg = TRUE; + break; + + + /* -i: Set flag so dot doesn't end non-SMTP input (same as -oi, seems + not to be documented for sendmail but mailx (at least) uses it) */ + + case 'i': + if (!*argrest) f.dot_ends = FALSE; else badarg = TRUE; + break; + + + /* -L: set the identifier used for syslog; equivalent to setting + syslog_processname in the config file, but needs to be an admin option. */ + + case 'L': + if (!*argrest) + if (++i < argc) argrest = argv[i]; else { badarg = TRUE; break; } + if ((sz = Ustrlen(argrest)) > 32) + exim_fail("exim: the -L syslog name is too long: \"%s\"\n", argrest); + if (sz < 1) + exim_fail("exim: the -L syslog name is too short\n"); + cmdline_syslog_name = string_copy_taint(argrest, GET_TAINTED); + break; + + case 'M': + receiving_message = FALSE; + + /* -MC: continue delivery of another message via an existing open + file descriptor. This option is used for an internal call by the + smtp transport when there is a pending message waiting to go to an + address to which it has got a connection. Five subsequent arguments are + required: transport name, host name, IP address, sequence number, and + message_id. Transports may decline to create new processes if the sequence + number gets too big. The channel is stdin. This (-MC) must be the last + argument. There's a subsequent check that the real-uid is privileged. + + If we are running in the test harness. delay for a bit, to let the process + that set this one up complete. This makes for repeatability of the logging, + etc. output. */ + + if (Ustrcmp(argrest, "C") == 0) + { + union sockaddr_46 interface_sock; + EXIM_SOCKLEN_T size = sizeof(interface_sock); + + if (argc != i + 6) + exim_fail("exim: too many or too few arguments after -MC\n"); + + if (msg_action_arg >= 0) + exim_fail("exim: incompatible arguments\n"); + + continue_transport = string_copy_taint( + exim_str_fail_toolong(argv[++i], EXIM_DRIVERNAME_MAX, "-C internal transport"), + GET_TAINTED); + continue_hostname = string_copy_taint( + exim_str_fail_toolong(argv[++i], EXIM_HOSTNAME_MAX, "-C internal hostname"), + GET_TAINTED); + continue_host_address = string_copy_taint( + exim_str_fail_toolong(argv[++i], EXIM_IPADDR_MAX, "-C internal hostaddr"), + GET_TAINTED); + continue_sequence = Uatoi(argv[++i]); + msg_action = MSG_DELIVER; + msg_action_arg = ++i; + forced_delivery = TRUE; + queue_run_pid = passed_qr_pid; + queue_run_pipe = passed_qr_pipe; + + if (!mac_ismsgid(argv[i])) + exim_fail("exim: malformed message id %s after -MC option\n", + argv[i]); + + /* Set up $sending_ip_address and $sending_port, unless proxied */ + + if (!continue_proxy_cipher) + if (getsockname(fileno(stdin), (struct sockaddr *)(&interface_sock), + &size) == 0) + sending_ip_address = host_ntoa(-1, &interface_sock, NULL, + &sending_port); + else + exim_fail("exim: getsockname() failed after -MC option: %s\n", + strerror(errno)); + + testharness_pause_ms(500); + break; + } + + else if (*argrest == 'C' && argrest[1] && !argrest[2]) + { + switch(argrest[1]) + { + /* -MCA: set the smtp_authenticated flag; this is useful only when it + precedes -MC (see above). The flag indicates that the host to which + Exim is connected has accepted an AUTH sequence. */ + + case 'A': f.smtp_authenticated = TRUE; break; + + /* -MCD: set the smtp_use_dsn flag; this indicates that the host + that exim is connected to supports the esmtp extension DSN */ + + case 'D': smtp_peer_options |= OPTION_DSN; break; + + /* -MCd: for debug, set a process-purpose string */ + + case 'd': if (++i < argc) + process_purpose = string_copy_taint( + exim_str_fail_toolong(argv[i], EXIM_DRIVERNAME_MAX, "-MCd"), + GET_TAINTED); + else badarg = TRUE; + break; + + /* -MCG: set the queue name, to a non-default value. Arguably, anything + from the commandline should be tainted - but we will need an untainted + value for the spoolfile when doing a -odi delivery process. */ + + case 'G': if (++i < argc) queue_name = string_copy_taint( + exim_str_fail_toolong(argv[i], EXIM_DRIVERNAME_MAX, "-MCG"), + GET_UNTAINTED); + else badarg = TRUE; + break; + + /* -MCK: the peer offered CHUNKING. Must precede -MC */ + + case 'K': smtp_peer_options |= OPTION_CHUNKING; break; + +#ifdef EXPERIMENTAL_ESMTP_LIMITS + /* -MCL: peer used LIMITS RCPTMAX and/or RCPTDOMAINMAX */ + case 'L': if (++i < argc) continue_limit_mail = Uatoi(argv[i]); + else badarg = TRUE; + if (++i < argc) continue_limit_rcpt = Uatoi(argv[i]); + else badarg = TRUE; + if (++i < argc) continue_limit_rcptdom = Uatoi(argv[i]); + else badarg = TRUE; + break; +#endif + + /* -MCP: set the smtp_use_pipelining flag; this is useful only when + it preceded -MC (see above) */ + + case 'P': smtp_peer_options |= OPTION_PIPE; break; + +#ifdef SUPPORT_SOCKS + /* -MCp: Socks proxy in use; nearside IP, port, external IP, port */ + case 'p': proxy_session = TRUE; + if (++i < argc) + { + proxy_local_address = string_copy_taint(argv[i], GET_TAINTED); + if (++i < argc) + { + proxy_local_port = Uatoi(argv[i]); + if (++i < argc) + { + proxy_external_address = string_copy_taint(argv[i], GET_TAINTED); + if (++i < argc) + { + proxy_external_port = Uatoi(argv[i]); + break; + } } } } + badarg = TRUE; + break; +#endif + /* -MCQ: pass on the pid of the queue-running process that started + this chain of deliveries and the fd of its synchronizing pipe; this + is useful only when it precedes -MC (see above) */ + + case 'Q': if (++i < argc) passed_qr_pid = (pid_t)(Uatol(argv[i])); + else badarg = TRUE; + if (++i < argc) passed_qr_pipe = (int)(Uatol(argv[i])); + else badarg = TRUE; + break; + + /* -MCq: do a quota check on the given recipient for the given size + of message. Separate from -MC. */ + case 'q': rcpt_verify_quota = TRUE; + if (++i < argc) message_size = Uatoi(argv[i]); + else badarg = TRUE; + break; + + /* -MCS: set the smtp_use_size flag; this is useful only when it + precedes -MC (see above) */ + + case 'S': smtp_peer_options |= OPTION_SIZE; break; + +#ifndef DISABLE_TLS + /* -MCs: used with -MCt; SNI was sent */ + /* -MCr: ditto, DANE */ + + case 'r': + case 's': if (++i < argc) + { + continue_proxy_sni = string_copy_taint( + exim_str_fail_toolong(argv[i], EXIM_HOSTNAME_MAX, "-MCr/-MCs"), + GET_TAINTED); + if (argrest[1] == 'r') continue_proxy_dane = TRUE; + } + else badarg = TRUE; + break; + + /* -MCt: similar to -MCT below but the connection is still open + via a proxy process which handles the TLS context and coding. + Require three arguments for the proxied local address and port, + and the TLS cipher. */ + + case 't': if (++i < argc) + sending_ip_address = string_copy_taint( + exim_str_fail_toolong(argv[i], EXIM_IPADDR_MAX, "-MCt IP"), + GET_TAINTED); + else badarg = TRUE; + if (++i < argc) + sending_port = (int)(Uatol(argv[i])); + else badarg = TRUE; + if (++i < argc) + continue_proxy_cipher = string_copy_taint( + exim_str_fail_toolong(argv[i], EXIM_CIPHERNAME_MAX, "-MCt cipher"), + GET_TAINTED); + else badarg = TRUE; + /*FALLTHROUGH*/ + + /* -MCT: set the tls_offered flag; this is useful only when it + precedes -MC (see above). The flag indicates that the host to which + Exim is connected has offered TLS support. */ + + case 'T': smtp_peer_options |= OPTION_TLS; break; +#endif + + default: badarg = TRUE; break; + } + break; + } + + /* -M[x]: various operations on the following list of message ids: + -M deliver the messages, ignoring next retry times and thawing + -Mc deliver the messages, checking next retry times, no thawing + -Mf freeze the messages + -Mg give up on the messages + -Mt thaw the messages + -Mrm remove the messages + In the above cases, this must be the last option. There are also the + following options which are followed by a single message id, and which + act on that message. Some of them use the "recipient" addresses as well. + -Mar add recipient(s) + -MG move to a different queue + -Mmad mark all recipients delivered + -Mmd mark recipients(s) delivered + -Mes edit sender + -Mset load a message for use with -be + -Mvb show body + -Mvc show copy (of whole message, in RFC 2822 format) + -Mvh show header + -Mvl show log + */ + + else if (!*argrest) + { + msg_action = MSG_DELIVER; + forced_delivery = f.deliver_force_thaw = TRUE; + } + else if (Ustrcmp(argrest, "ar") == 0) + { + msg_action = MSG_ADD_RECIPIENT; + one_msg_action = TRUE; + } + else if (Ustrcmp(argrest, "c") == 0) msg_action = MSG_DELIVER; + else if (Ustrcmp(argrest, "es") == 0) + { + msg_action = MSG_EDIT_SENDER; + one_msg_action = TRUE; + } + else if (Ustrcmp(argrest, "f") == 0) msg_action = MSG_FREEZE; + else if (Ustrcmp(argrest, "g") == 0) + { + msg_action = MSG_DELIVER; + deliver_give_up = TRUE; + } + else if (Ustrcmp(argrest, "G") == 0) + { + msg_action = MSG_SETQUEUE; + queue_name_dest = string_copy_taint( + exim_str_fail_toolong(argv[++i], EXIM_DRIVERNAME_MAX, "-MG"), + GET_TAINTED); + } + else if (Ustrcmp(argrest, "mad") == 0) msg_action = MSG_MARK_ALL_DELIVERED; + else if (Ustrcmp(argrest, "md") == 0) + { + msg_action = MSG_MARK_DELIVERED; + one_msg_action = TRUE; + } + else if (Ustrcmp(argrest, "rm") == 0) msg_action = MSG_REMOVE; + else if (Ustrcmp(argrest, "set") == 0) + { + msg_action = MSG_LOAD; + one_msg_action = TRUE; + } + else if (Ustrcmp(argrest, "t") == 0) msg_action = MSG_THAW; + else if (Ustrcmp(argrest, "vb") == 0) + { + msg_action = MSG_SHOW_BODY; + one_msg_action = TRUE; + } + else if (Ustrcmp(argrest, "vc") == 0) + { + msg_action = MSG_SHOW_COPY; + one_msg_action = TRUE; + } + else if (Ustrcmp(argrest, "vh") == 0) + { + msg_action = MSG_SHOW_HEADER; + one_msg_action = TRUE; + } + else if (Ustrcmp(argrest, "vl") == 0) + { + msg_action = MSG_SHOW_LOG; + one_msg_action = TRUE; + } + else { badarg = TRUE; break; } + + /* All the -Mxx options require at least one message id. */ + + msg_action_arg = i + 1; + if (msg_action_arg >= argc) + exim_fail("exim: no message ids given after %s option\n", arg); + + /* Some require only message ids to follow */ + + if (!one_msg_action) + { + for (int j = msg_action_arg; j < argc; j++) if (!mac_ismsgid(argv[j])) + exim_fail("exim: malformed message id %s after %s option\n", + argv[j], arg); + goto END_ARG; /* Remaining args are ids */ + } + + /* Others require only one message id, possibly followed by addresses, + which will be handled as normal arguments. */ + + else + { + if (!mac_ismsgid(argv[msg_action_arg])) + exim_fail("exim: malformed message id %s after %s option\n", + argv[msg_action_arg], arg); + i++; + } + break; + + + /* Some programs seem to call the -om option without the leading o; + for sendmail it askes for "me too". Exim always does this. */ + + case 'm': + if (*argrest) badarg = TRUE; + break; + + + /* -N: don't do delivery - a debugging option that stops transports doing + their thing. It implies debugging at the D_v level. */ + + case 'N': + if (!*argrest) + { + f.dont_deliver = TRUE; + debug_selector |= D_v; + debug_file = stderr; + } + else badarg = TRUE; + break; + + + /* -n: This means "don't alias" in sendmail, apparently. + For normal invocations, it has no effect. + It may affect some other options. */ + + case 'n': + flag_n = TRUE; + break; + + /* -O: Just ignore it. In sendmail, apparently -O option=value means set + option to the specified value. This form uses long names. We need to handle + -O option=value and -Ooption=value. */ + + case 'O': + if (!*argrest) + if (++i >= argc) + exim_fail("exim: string expected after -O\n"); + break; + + case 'o': + switch (*argrest++) + { + /* -oA: Set an argument for the bi command (sendmail's "alternate alias + file" option). */ + case 'A': + if (!*(alias_arg = argrest)) + if (i+1 < argc) alias_arg = argv[++i]; + else exim_fail("exim: string expected after -oA\n"); + break; + + /* -oB: Set a connection message max value for remote deliveries */ + case 'B': + { + uschar * p = argrest; + if (!*p) + if (i+1 < argc && isdigit((argv[i+1][0]))) + p = argv[++i]; + else + { + connection_max_messages = 1; + p = NULL; + } + + if (p) + { + if (!isdigit(*p)) + exim_fail("exim: number expected after -oB\n"); + connection_max_messages = Uatoi(p); + } + } + break; + + /* -odb: background delivery */ + + case 'd': + if (Ustrcmp(argrest, "b") == 0) + { + f.synchronous_delivery = FALSE; + arg_queue_only = FALSE; + queue_only_set = TRUE; + } + + /* -odd: testsuite-only: add no inter-process delays */ + + else if (Ustrcmp(argrest, "d") == 0) + f.testsuite_delays = FALSE; + + /* -odf: foreground delivery (smail-compatible option); same effect as + -odi: interactive (synchronous) delivery (sendmail-compatible option) + */ + + else if (Ustrcmp(argrest, "f") == 0 || Ustrcmp(argrest, "i") == 0) + { + f.synchronous_delivery = TRUE; + arg_queue_only = FALSE; + queue_only_set = TRUE; + } + + /* -odq: queue only */ + + else if (Ustrcmp(argrest, "q") == 0) + { + f.synchronous_delivery = FALSE; + arg_queue_only = TRUE; + queue_only_set = TRUE; + } + + /* -odqs: queue SMTP only - do local deliveries and remote routing, + but no remote delivery */ + + else if (Ustrcmp(argrest, "qs") == 0) + { + f.queue_smtp = TRUE; + arg_queue_only = FALSE; + queue_only_set = TRUE; + } + else badarg = TRUE; + break; + + /* -oex: Sendmail error flags. As these are also accepted without the + leading -o prefix, for compatibility with vacation and other callers, + they are handled with -e above. */ + + /* -oi: Set flag so dot doesn't end non-SMTP input (same as -i) + -oitrue: Another sendmail syntax for the same */ + + case 'i': + if (!*argrest || Ustrcmp(argrest, "true") == 0) + f.dot_ends = FALSE; + else badarg = TRUE; + break; + + /* -oM*: Set various characteristics for an incoming message; actually + acted on for trusted callers only. */ + + case 'M': + { + if (i+1 >= argc) + exim_fail("exim: data expected after -oM%s\n", argrest); + + /* -oMa: Set sender host address */ + + if (Ustrcmp(argrest, "a") == 0) + sender_host_address = string_copy_taint( + exim_str_fail_toolong(argv[++i], EXIM_IPADDR_MAX, "-oMa"), + GET_TAINTED); + + /* -oMaa: Set authenticator name */ + + else if (Ustrcmp(argrest, "aa") == 0) + sender_host_authenticated = string_copy_taint( + exim_str_fail_toolong(argv[++i], EXIM_DRIVERNAME_MAX, "-oMaa"), + GET_TAINTED); + + /* -oMas: setting authenticated sender */ + + else if (Ustrcmp(argrest, "as") == 0) + authenticated_sender = string_copy_taint( + exim_str_fail_toolong(argv[++i], EXIM_EMAILADDR_MAX, "-oMas"), + GET_TAINTED); + + /* -oMai: setting authenticated id */ + + else if (Ustrcmp(argrest, "ai") == 0) + authenticated_id = string_copy_taint( + exim_str_fail_toolong(argv[++i], EXIM_EMAILADDR_MAX, "-oMas"), + GET_TAINTED); + + /* -oMi: Set incoming interface address */ + + else if (Ustrcmp(argrest, "i") == 0) + interface_address = string_copy_taint( + exim_str_fail_toolong(argv[++i], EXIM_IPADDR_MAX, "-oMi"), + GET_TAINTED); + + /* -oMm: Message reference */ + + else if (Ustrcmp(argrest, "m") == 0) + { + if (!mac_ismsgid(argv[i+1])) + exim_fail("-oMm must be a valid message ID\n"); + if (!f.trusted_config) + exim_fail("-oMm must be called by a trusted user/config\n"); + message_reference = argv[++i]; + } + + /* -oMr: Received protocol */ + + else if (Ustrcmp(argrest, "r") == 0) + + if (received_protocol) + exim_fail("received_protocol is set already\n"); + else + received_protocol = string_copy_taint( + exim_str_fail_toolong(argv[++i], EXIM_DRIVERNAME_MAX, "-oMr"), + GET_TAINTED); + + /* -oMs: Set sender host name */ + + else if (Ustrcmp(argrest, "s") == 0) + sender_host_name = string_copy_taint( + exim_str_fail_toolong(argv[++i], EXIM_HOSTNAME_MAX, "-oMs"), + GET_TAINTED); + + /* -oMt: Set sender ident */ + + else if (Ustrcmp(argrest, "t") == 0) + { + sender_ident_set = TRUE; + sender_ident = string_copy_taint( + exim_str_fail_toolong(argv[++i], EXIM_IDENTUSER_MAX, "-oMt"), + GET_TAINTED); + } + + /* Else a bad argument */ + + else + badarg = TRUE; + } + break; + + /* -om: Me-too flag for aliases. Exim always does this. Some programs + seem to call this as -m (undocumented), so that is also accepted (see + above). */ + /* -oo: An ancient flag for old-style addresses which still seems to + crop up in some calls (see in SCO). */ + + case 'm': + case 'o': + if (*argrest) badarg = TRUE; + break; + + /* -oP : set pid file path for daemon + -oPX: delete pid file of daemon */ + + case 'P': + if (!f.running_in_test_harness && real_uid != root_uid && real_uid != exim_uid) + exim_fail("exim: only uid=%d or uid=%d can use -oP and -oPX " + "(uid=%d euid=%d | %d)\n", + root_uid, exim_uid, getuid(), geteuid(), real_uid); + if (!*argrest) override_pid_file_path = argv[++i]; + else if (Ustrcmp(argrest, "X") == 0) delete_pid_file(); + else badarg = TRUE; + break; + + + /* -or : set timeout for non-SMTP acceptance + -os : set timeout for SMTP acceptance */ + + case 'r': + case 's': + { + int * tp = argrest[-1] == 'r' + ? &arg_receive_timeout : &arg_smtp_receive_timeout; + if (*argrest) + *tp = readconf_readtime(argrest, 0, FALSE); + else if (i+1 < argc) + *tp = readconf_readtime(argv[++i], 0, FALSE); + + if (*tp < 0) + exim_fail("exim: bad time value %s: abandoned\n", argv[i]); + } + break; + + /* -oX : Override local_interfaces and/or default daemon ports */ + /* Limits: Is there a real limit we want here? 1024 is very arbitrary. */ + + case 'X': + if (*argrest) badarg = TRUE; + else override_local_interfaces = string_copy_taint( + exim_str_fail_toolong(argv[++i], 1024, "-oX"), + GET_TAINTED); + break; + + /* -oY: Override creation of daemon notifier socket */ + + case 'Y': + if (*argrest) badarg = TRUE; + else notifier_socket = NULL; + break; + + /* Unknown -o argument */ + + default: + badarg = TRUE; + } + break; + + + /* -ps: force Perl startup; -pd force delayed Perl startup */ + + case 'p': + #ifdef EXIM_PERL + if (*argrest == 's' && argrest[1] == 0) + { + perl_start_option = 1; + break; + } + if (*argrest == 'd' && argrest[1] == 0) + { + perl_start_option = -1; + break; + } + #endif + + /* -panythingelse is taken as the Sendmail-compatible argument -prval:sval, + which sets the host protocol and host name */ + + if (!*argrest) + if (i+1 < argc) argrest = argv[++i]; else { badarg = TRUE; break; } + + if (*argrest) + { + uschar * hn = Ustrchr(argrest, ':'); + + if (received_protocol) + exim_fail("received_protocol is set already\n"); + + if (!hn) + received_protocol = string_copy_taint( + exim_str_fail_toolong(argrest, EXIM_DRIVERNAME_MAX, "-p"), + GET_TAINTED); + else + { + (void) exim_str_fail_toolong(argrest, (EXIM_DRIVERNAME_MAX+1+EXIM_HOSTNAME_MAX), "-p:"); + received_protocol = string_copyn_taint(argrest, hn - argrest, GET_TAINTED); + sender_host_name = string_copy_taint(hn + 1, GET_TAINTED); + } + } + break; + + + case 'q': + receiving_message = FALSE; + if (queue_interval >= 0) + exim_fail("exim: -q specified more than once\n"); + + /* -qq...: Do queue runs in a 2-stage manner */ + + if (*argrest == 'q') + { + f.queue_2stage = TRUE; + argrest++; + } + + /* -qi...: Do only first (initial) deliveries */ + + if (*argrest == 'i') + { + f.queue_run_first_delivery = TRUE; + argrest++; + } + + /* -qf...: Run the queue, forcing deliveries + -qff..: Ditto, forcing thawing as well */ + + if (*argrest == 'f') + { + f.queue_run_force = TRUE; + if (*++argrest == 'f') + { + f.deliver_force_thaw = TRUE; + argrest++; + } + } + + /* -q[f][f]l...: Run the queue only on local deliveries */ + + if (*argrest == 'l') + { + f.queue_run_local = TRUE; + argrest++; + } + + /* -q[f][f][l][G]... Work on the named queue */ + + if (*argrest == 'G') + { + int i; + for (argrest++, i = 0; argrest[i] && argrest[i] != '/'; ) i++; + exim_len_fail_toolong(i, EXIM_DRIVERNAME_MAX, "-q*G"); + queue_name = string_copyn(argrest, i); + argrest += i; + if (*argrest == '/') argrest++; + } + + /* -q[f][f][l][G]: Run the queue, optionally forced, optionally local + only, optionally named, optionally starting from a given message id. */ + + if (!(list_queue || count_queue)) + if ( !*argrest + && (i + 1 >= argc || argv[i+1][0] == '-' || mac_ismsgid(argv[i+1]))) + { + queue_interval = 0; + if (i+1 < argc && mac_ismsgid(argv[i+1])) + start_queue_run_id = string_copy_taint(argv[++i], GET_TAINTED); + if (i+1 < argc && mac_ismsgid(argv[i+1])) + stop_queue_run_id = string_copy_taint(argv[++i], GET_TAINTED); + } + + /* -q[f][f][l][G/]: Run the queue at regular intervals, optionally + forced, optionally local only, optionally named. */ + + else if ((queue_interval = readconf_readtime(*argrest ? argrest : argv[++i], + 0, FALSE)) <= 0) + exim_fail("exim: bad time value %s: abandoned\n", argv[i]); + break; + + + case 'R': /* Synonymous with -qR... */ + { + const uschar *tainted_selectstr; + + receiving_message = FALSE; + + /* -Rf: As -R (below) but force all deliveries, + -Rff: Ditto, but also thaw all frozen messages, + -Rr: String is regex + -Rrf: Regex and force + -Rrff: Regex and force and thaw + + in all cases provided there are no further characters in this + argument. */ + + if (*argrest) + for (int i = 0; i < nelem(rsopts); i++) + if (Ustrcmp(argrest, rsopts[i]) == 0) + { + if (i != 2) f.queue_run_force = TRUE; + if (i >= 2) f.deliver_selectstring_regex = TRUE; + if (i == 1 || i == 4) f.deliver_force_thaw = TRUE; + argrest += Ustrlen(rsopts[i]); + } + + /* -R: Set string to match in addresses for forced queue run to + pick out particular messages. */ + + /* Avoid attacks from people providing very long strings, and do so before + we make copies. */ + if (*argrest) + tainted_selectstr = argrest; + else if (i+1 < argc) + tainted_selectstr = argv[++i]; + else + exim_fail("exim: string expected after -R\n"); + deliver_selectstring = string_copy_taint( + exim_str_fail_toolong(tainted_selectstr, EXIM_EMAILADDR_MAX, "-R"), + GET_TAINTED); + } + break; + + /* -r: an obsolete synonym for -f (see above) */ + + + /* -S: Like -R but works on sender. */ + + case 'S': /* Synonymous with -qS... */ + { + const uschar *tainted_selectstr; + + receiving_message = FALSE; + + /* -Sf: As -S (below) but force all deliveries, + -Sff: Ditto, but also thaw all frozen messages, + -Sr: String is regex + -Srf: Regex and force + -Srff: Regex and force and thaw + + in all cases provided there are no further characters in this + argument. */ + + if (*argrest) + for (int i = 0; i < nelem(rsopts); i++) + if (Ustrcmp(argrest, rsopts[i]) == 0) + { + if (i != 2) f.queue_run_force = TRUE; + if (i >= 2) f.deliver_selectstring_sender_regex = TRUE; + if (i == 1 || i == 4) f.deliver_force_thaw = TRUE; + argrest += Ustrlen(rsopts[i]); + } + + /* -S: Set string to match in addresses for forced queue run to + pick out particular messages. */ + + if (*argrest) + tainted_selectstr = argrest; + else if (i+1 < argc) + tainted_selectstr = argv[++i]; + else + exim_fail("exim: string expected after -S\n"); + deliver_selectstring_sender = string_copy_taint( + exim_str_fail_toolong(tainted_selectstr, EXIM_EMAILADDR_MAX, "-S"), + GET_TAINTED); + } + break; + + /* -Tqt is an option that is exclusively for use by the testing suite. + It is not recognized in other circumstances. It allows for the setting up + of explicit "queue times" so that various warning/retry things can be + tested. Otherwise variability of clock ticks etc. cause problems. */ + + case 'T': + if (f.running_in_test_harness && Ustrcmp(argrest, "qt") == 0) + fudged_queue_times = string_copy_taint(argv[++i], GET_TAINTED); + else badarg = TRUE; + break; + + + /* -t: Set flag to extract recipients from body of message. */ + + case 't': + if (!*argrest) extract_recipients = TRUE; + + /* -ti: Set flag to extract recipients from body of message, and also + specify that dot does not end the message. */ + + else if (Ustrcmp(argrest, "i") == 0) + { + extract_recipients = TRUE; + f.dot_ends = FALSE; + } + + /* -tls-on-connect: don't wait for STARTTLS (for old clients) */ + + #ifndef DISABLE_TLS + else if (Ustrcmp(argrest, "ls-on-connect") == 0) tls_in.on_connect = TRUE; + #endif + + else badarg = TRUE; + break; + + + /* -U: This means "initial user submission" in sendmail, apparently. The + doc claims that in future sendmail may refuse syntactically invalid + messages instead of fixing them. For the moment, we just ignore it. */ + + case 'U': + break; + + + /* -v: verify things - this is a very low-level debugging */ + + case 'v': + if (!*argrest) + { + debug_selector |= D_v; + debug_file = stderr; + } + else badarg = TRUE; + break; + + + /* -x: AIX uses this to indicate some fancy 8-bit character stuff: + + The -x flag tells the sendmail command that mail from a local + mail program has National Language Support (NLS) extended characters + in the body of the mail item. The sendmail command can send mail with + extended NLS characters across networks that normally corrupts these + 8-bit characters. + + As Exim is 8-bit clean, it just ignores this flag. */ + + case 'x': + if (*argrest) badarg = TRUE; + break; + + /* -X: in sendmail: takes one parameter, logfile, and sends debugging + logs to that file. We swallow the parameter and otherwise ignore it. */ + + case 'X': + if (!*argrest) + if (++i >= argc) + exim_fail("exim: string expected after -X\n"); + break; + + /* -z: a line of text to log */ + + case 'z': + if (!*argrest) + if (++i < argc) + log_oneline = string_copy_taint( + exim_str_fail_toolong(argv[i], 2048, "-z logtext"), + GET_TAINTED); + else + exim_fail("exim: file name expected after %s\n", argv[i-1]); + break; + + /* All other initial characters are errors */ + + default: + badarg = TRUE; + break; + } /* End of high-level switch statement */ + + /* Failed to recognize the option, or syntax error */ + + if (badarg) + exim_fail("exim abandoned: unknown, malformed, or incomplete " + "option %s\n", arg); + } + + +/* If -R or -S have been specified without -q, assume a single queue run. */ + + if ( (deliver_selectstring || deliver_selectstring_sender) + && queue_interval < 0) + queue_interval = 0; + + +END_ARG: + store_pool = old_pool; + } + +/* If usage_wanted is set we call the usage function - which never returns */ +if (usage_wanted) exim_usage(called_as); + +/* Arguments have been processed. Check for incompatibilities. */ +if ( ( (smtp_input || extract_recipients || recipients_arg < argc) + && ( f.daemon_listen || queue_interval >= 0 || bi_option + || test_retry_arg >= 0 || test_rewrite_arg >= 0 + || filter_test != FTEST_NONE + || msg_action_arg > 0 && !one_msg_action + ) ) + || ( msg_action_arg > 0 + && ( f.daemon_listen || queue_interval > 0 || list_options + || checking && msg_action != MSG_LOAD + || bi_option || test_retry_arg >= 0 || test_rewrite_arg >= 0 + ) ) + || ( (f.daemon_listen || queue_interval > 0) + && ( sender_address || list_options || list_queue || checking + || bi_option + ) ) + || f.daemon_listen && queue_interval == 0 + || f.inetd_wait_mode && queue_interval >= 0 + || ( list_options + && ( checking || smtp_input || extract_recipients + || filter_test != FTEST_NONE || bi_option + ) ) + || ( verify_address_mode + && ( f.address_test_mode || smtp_input || extract_recipients + || filter_test != FTEST_NONE || bi_option + ) ) + || ( f.address_test_mode + && ( smtp_input || extract_recipients || filter_test != FTEST_NONE + || bi_option + ) ) + || ( smtp_input + && (sender_address || filter_test != FTEST_NONE || extract_recipients) + ) + || deliver_selectstring && queue_interval < 0 + || msg_action == MSG_LOAD && (!expansion_test || expansion_test_message) + ) + exim_fail("exim: incompatible command-line options or arguments\n"); + +/* If debugging is set up, set the file and the file descriptor to pass on to +child processes. It should, of course, be 2 for stderr. Also, force the daemon +to run in the foreground. */ + +if (debug_selector != 0) + { + debug_file = stderr; + debug_fd = fileno(debug_file); + f.background_daemon = FALSE; + testharness_pause_ms(100); /* lets caller finish */ + if (debug_selector != D_v) /* -v only doesn't show this */ + { + debug_printf("Exim version %s uid=%ld gid=%ld pid=%d D=%x\n", + version_string, (long int)real_uid, (long int)real_gid, (int)getpid(), + debug_selector); + if (!version_printed) + show_whats_supported(FALSE); + } + } + +/* When started with root privilege, ensure that the limits on the number of +open files and the number of processes (where that is accessible) are +sufficiently large, or are unset, in case Exim has been called from an +environment where the limits are screwed down. Not all OS have the ability to +change some of these limits. */ + +if (unprivileged) + { + DEBUG(D_any) debug_print_ids(US"Exim has no root privilege:"); + } +else + { + struct rlimit rlp; + +#ifdef RLIMIT_NOFILE + if (getrlimit(RLIMIT_NOFILE, &rlp) < 0) + { + log_write(0, LOG_MAIN|LOG_PANIC, "getrlimit(RLIMIT_NOFILE) failed: %s", + strerror(errno)); + rlp.rlim_cur = rlp.rlim_max = 0; + } + + /* I originally chose 1000 as a nice big number that was unlikely to + be exceeded. It turns out that some older OS have a fixed upper limit of + 256. */ + + if (rlp.rlim_cur < 1000) + { + rlp.rlim_cur = rlp.rlim_max = 1000; + if (setrlimit(RLIMIT_NOFILE, &rlp) < 0) + { + rlp.rlim_cur = rlp.rlim_max = 256; + if (setrlimit(RLIMIT_NOFILE, &rlp) < 0) + log_write(0, LOG_MAIN|LOG_PANIC, "setrlimit(RLIMIT_NOFILE) failed: %s", + strerror(errno)); + } + } +#endif + +#ifdef RLIMIT_NPROC + if (getrlimit(RLIMIT_NPROC, &rlp) < 0) + { + log_write(0, LOG_MAIN|LOG_PANIC, "getrlimit(RLIMIT_NPROC) failed: %s", + strerror(errno)); + rlp.rlim_cur = rlp.rlim_max = 0; + } + +# ifdef RLIM_INFINITY + if (rlp.rlim_cur != RLIM_INFINITY && rlp.rlim_cur < 1000) + { + rlp.rlim_cur = rlp.rlim_max = RLIM_INFINITY; +# else + if (rlp.rlim_cur < 1000) + { + rlp.rlim_cur = rlp.rlim_max = 1000; +# endif + if (setrlimit(RLIMIT_NPROC, &rlp) < 0) + log_write(0, LOG_MAIN|LOG_PANIC, "setrlimit(RLIMIT_NPROC) failed: %s", + strerror(errno)); + } +#endif + } + +/* Exim is normally entered as root (but some special configurations are +possible that don't do this). However, it always spins off sub-processes that +set their uid and gid as required for local delivery. We don't want to pass on +any extra groups that root may belong to, so we want to get rid of them all at +this point. + +We need to obey setgroups() at this stage, before possibly giving up root +privilege for a changed configuration file, but later on we might need to +check on the additional groups for the admin user privilege - can't do that +till after reading the config, which might specify the exim gid. Therefore, +save the group list here first. */ + +if ((group_count = getgroups(nelem(group_list), group_list)) < 0) + exim_fail("exim: getgroups() failed: %s\n", strerror(errno)); + +/* There is a fundamental difference in some BSD systems in the matter of +groups. FreeBSD and BSDI are known to be different; NetBSD and OpenBSD are +known not to be different. On the "different" systems there is a single group +list, and the first entry in it is the current group. On all other versions of +Unix there is a supplementary group list, which is in *addition* to the current +group. Consequently, to get rid of all extraneous groups on a "standard" system +you pass over 0 groups to setgroups(), while on a "different" system you pass +over a single group - the current group, which is always the first group in the +list. Calling setgroups() with zero groups on a "different" system results in +an error return. The following code should cope with both types of system. + + Unfortunately, recent MacOS, which should be a FreeBSD, "helpfully" succeeds + the "setgroups() with zero groups" - and changes the egid. + Thanks to that we had to stash the original_egid above, for use below + in the call to exim_setugid(). + +However, if this process isn't running as root, setgroups() can't be used +since you have to be root to run it, even if throwing away groups. +Except, sigh, for Hurd - where you can. +Not being root here happens only in some unusual configurations. */ + +if ( !unprivileged +#ifndef OS_SETGROUPS_ZERO_DROPS_ALL + && setgroups(0, NULL) != 0 +#endif + && setgroups(1, group_list) != 0) + exim_fail("exim: setgroups() failed: %s\n", strerror(errno)); + +/* If the configuration file name has been altered by an argument on the +command line (either a new file name or a macro definition) and the caller is +not root, or if this is a filter testing run, remove any setuid privilege the +program has and run as the underlying user. + +The exim user is locked out of this, which severely restricts the use of -C +for some purposes. + +Otherwise, set the real ids to the effective values (should be root unless run +from inetd, which it can either be root or the exim uid, if one is configured). + +There is a private mechanism for bypassing some of this, in order to make it +possible to test lots of configurations automatically, without having either to +recompile each time, or to patch in an actual configuration file name and other +values (such as the path name). If running in the test harness, pretend that +configuration file changes and macro definitions haven't happened. */ + +if (( /* EITHER */ + (!f.trusted_config || /* Config changed, or */ + !macros_trusted(opt_D_used)) && /* impermissible macros and */ + real_uid != root_uid && /* Not root, and */ + !f.running_in_test_harness /* Not fudged */ + ) || /* OR */ + expansion_test /* expansion testing */ + || /* OR */ + filter_test != FTEST_NONE) /* Filter testing */ + { + setgroups(group_count, group_list); + exim_setugid(real_uid, real_gid, FALSE, + US"-C, -D, -be or -bf forces real uid"); + removed_privilege = TRUE; + + /* In the normal case when Exim is called like this, stderr is available + and should be used for any logging information because attempts to write + to the log will usually fail. To arrange this, we unset really_exim. However, + if no stderr is available there is no point - we might as well have a go + at the log (if it fails, syslog will be written). + + Note that if the invoker is Exim, the logs remain available. Messing with + this causes unlogged successful deliveries. */ + + if (log_stderr && real_uid != exim_uid) + f.really_exim = FALSE; + } + +/* Privilege is to be retained for the moment. It may be dropped later, +depending on the job that this Exim process has been asked to do. For now, set +the real uid to the effective so that subsequent re-execs of Exim are done by a +privileged user. */ + +else + exim_setugid(geteuid(), original_egid, FALSE, US"forcing real = effective"); + +/* If testing a filter, open the file(s) now, before wasting time doing other +setups and reading the message. */ + +if (filter_test & FTEST_SYSTEM) + if ((filter_sfd = Uopen(filter_test_sfile, O_RDONLY, 0)) < 0) + exim_fail("exim: failed to open %s: %s\n", filter_test_sfile, + strerror(errno)); + +if (filter_test & FTEST_USER) + if ((filter_ufd = Uopen(filter_test_ufile, O_RDONLY, 0)) < 0) + exim_fail("exim: failed to open %s: %s\n", filter_test_ufile, + strerror(errno)); + +/* Initialise lookup_list +If debugging, already called above via version reporting. +In either case, we initialise the list of available lookups while running +as root. All dynamically modules are loaded from a directory which is +hard-coded into the binary and is code which, if not a module, would be +part of Exim already. Ability to modify the content of the directory +is equivalent to the ability to modify a setuid binary! + +This needs to happen before we read the main configuration. */ +init_lookup_list(); + +/*XXX this excrescence could move to the testsuite standard config setup file */ +#ifdef SUPPORT_I18N +if (f.running_in_test_harness) smtputf8_advertise_hosts = NULL; +#endif + +/* Read the main runtime configuration data; this gives up if there +is a failure. It leaves the configuration file open so that the subsequent +configuration data for delivery can be read if needed. + +NOTE: immediately after opening the configuration file we change the working +directory to "/"! Later we change to $spool_directory. We do it there, because +during readconf_main() some expansion takes place already. */ + +/* Store the initial cwd before we change directories. Can be NULL if the +dir has already been unlinked. */ +initial_cwd = os_getcwd(NULL, 0); +if (!initial_cwd && errno) + exim_fail("exim: getting initial cwd failed: %s\n", strerror(errno)); + +if (initial_cwd && (strlen(CCS initial_cwd) >= BIG_BUFFER_SIZE)) + exim_fail("exim: initial cwd is far too long (%d)\n", Ustrlen(CCS initial_cwd)); + +/* checking: + -be[m] expansion test - + -b[fF] filter test new + -bh[c] host test - + -bmalware malware_test_file new + -brt retry test new + -brw rewrite test new + -bt address test - + -bv[s] address verify - + list_options: + -bP