]> git.ipfire.org Git - thirdparty/postfix.git/commitdiff
postfix-3.11-20250323
authorWietse Z Venema <wietse@porcupine.org>
Sun, 23 Mar 2025 05:00:00 +0000 (00:00 -0500)
committerViktor Dukhovni <ietf-dane@dukhovni.org>
Tue, 25 Mar 2025 08:34:34 +0000 (19:34 +1100)
12 files changed:
postfix/HISTORY
postfix/README_FILES/TLSRPT_README
postfix/html/TLSRPT_README.html
postfix/proto/TLSRPT_README.html
postfix/proto/stop.spell-history
postfix/src/global/Makefile.in
postfix/src/global/dict_sqlite.c
postfix/src/global/dict_sqlite.h
postfix/src/global/dict_sqlite_test.c [new file with mode: 0644]
postfix/src/global/mail_version.h
postfix/src/postconf/postconf_edit.c
postfix/src/util/vstring_vstream.c

index 955c282adb290d50542572e6f9d5fdaab618f956..94c94d7b37ba7836e5b83b9bb639c97c3d84f21c 100644 (file)
@@ -29015,3 +29015,32 @@ Apologies for any names omitted.
        Dovecot auth client did not attempt to create a new connection
        after an I/O error on an existing connection. Reported by
        Oleksandr Kozmenko. File: xsasl/xsasl_dovecot_server.c.
+
+20240315
+
+       Code health: two typos canceled each other's effect. Fix
+       by Michael Tokarev. No change in compiler output. File:
+       util/vstring_vstream.c.
+
+20250316
+
+       Bugfix (defect introduced: date 19991116): when appending a
+       setting to a main.cf or master.cf file that did not end in
+       a newline character, the "postconf -e" command did not add
+       an extra newline character before appending the new setting,
+       causing information to become garbled. Fix by Michael
+       Tokarev. File: postconf/postconf_edit.c.
+
+20259317
+
+       Documentation: added text to clarify the difference between
+       SMTP connection reuse and TLS session resumption, and that
+       these can be combined together. File: proto/TLSRPT_README.html.
+
+20250321
+
+       Safety: the SQLite client now logs a warning when a query
+       uses double quotes instead of the Postfix-recommended single
+       quotes. Oscar Bataille reported that the non-recommended
+       form is not protected against SQL injection. Files:
+       global/dict_sqlite.c, global/dict_sqlite_test.c.
index 91e72b526cd410e561a9f8f512f827692a495b86..4a2e88f356d4fe5bc790b895102d072c20d43e67 100644 (file)
@@ -7,6 +7,7 @@ T\bTa\bab\bbl\ble\be o\bof\bf C\bCo\bon\bnt\bte\ben\bnt\bts\bs
   * Introduction
   * Building Postfix with TLSRPT support
   * Turning on TLSRPT
+  * Connection reuse versus session resumption
   * TLSRPT Status logging
   * Delivering TLSRPT summaries via email
   * MTA-STS Support via smtp_tls_policy_maps
@@ -27,7 +28,7 @@ A policy for domain example.com could look like this:
 
 Instead of mailto:, a policy may specify an https: destination.
 
-The diagram below shows how Postfix TLS handshake success and failure events
+The diagram below shows how successful or failed Postfix TLS handshake events
 are collected and processed into daily summary reports.
 
      Postfix SMTP and      TLSRPT client     TLSRPT collector,    Email or HTTP
@@ -124,6 +125,36 @@ Notes:
 For details on how to run the TLSRPT collection and reporting infrastructure,
 see the documentation at https://github.com/sys4/tlsrpt-reporter.
 
+C\bCo\bon\bnn\bne\bec\bct\bti\bio\bon\bn r\bre\beu\bus\bse\be v\bve\ber\brs\bsu\bus\bs s\bse\bes\bss\bsi\bio\bon\bn r\bre\bes\bsu\bum\bmp\bpt\bti\bio\bon\bn
+
+The Postfix SMTP client implements two kinds of reuse:
+
+  * S\bSM\bMT\bTP\bP C\bCo\bon\bnn\bne\bec\bct\bti\bio\bon\bn r\bre\beu\bus\bse\be:\b: a Postfix SMTP client creates a new SMTP connection,
+    sends one email message, and saves the connection instead of closing it.
+    Later, some SMTP client reuses that connection, sends an email message, and
+    saves or closes the connection depending on whether it has reached some
+    reuse limit. Each connection can be used by only one Postfix SMTP client at
+    a time.
+
+  * T\bTL\bLS\bS S\bSe\bes\bss\bsi\bio\bon\bn r\bre\bes\bsu\bum\bmp\bpt\bti\bio\bon\bn:\b: a Postfix SMTP client saves the result from a "new"
+    TLS handshake. Later, one or more SMTP clients create a new SMTP connection
+    and resume the saved TLS session on their new connection.
+
+Of course there is a third case:
+
+  * C\bCo\bom\bmb\bbi\bin\bne\bed\bd r\bre\beu\bus\bse\be a\ban\bnd\bd r\bre\bes\bsu\bum\bmp\bpt\bti\bio\bon\bn:\b: a Postfix SMTP client creates a new SMTP
+    connection, sends one email message, saves the result from a "new" TLS
+    handshake, and also saves the connection instead of closing it. Later, one
+    SMTP client reuses (and saves) that connection, one client at a time, and
+    one or more clients create a new SMTP connection and resume the saved TLS
+    session on their new connection.
+
+In all cases, there is no TLS handshake when a saved SMTP connection is reused,
+and there is no "new" TLS handshake when a saved TLS session is resumed.
+
+As described next, Postfix will by default log and generate only a TLSRPT event
+for a "new" TLS handshake.
+
 T\bTL\bLS\bSR\bRP\bPT\bT S\bSt\bta\bat\btu\bus\bs l\blo\bog\bgg\bgi\bin\bng\bg
 
 With TLSRPT support turned on, the Postfix TLSRPT client will not only report
index 877c41e3a3de1f5ea8ee7581b2bf8516ac39cf40..6d3746e4c81a18a4ce7379708f8ca5b7e994cd29 100644 (file)
@@ -25,6 +25,7 @@
 <li> <a href="#intro"> Introduction </a> </li>
 <li> <a href="#building"> Building Postfix with TLSRPT support </a>
 <li> <a href="#using"> Turning on TLSRPT </a> </li>
+<li> <a href="#reusing"> Connection reuse versus session resumption </a> </li>
 <li> <a href="#logging"> TLSRPT Status logging </a> </li>
 <li> <a href="#delivering"> Delivering TLSRPT summaries via email</a> </li>
 <li> <a href="#mta-sts"> MTA-STS Support via smtp_tls_policy_maps </a> </li>
@@ -52,8 +53,8 @@ _smtp._tls.example.com. IN TXT "v=TLSRPTv1; rua=mailto:smtp-tls-report@example.c
 <p> Instead of <tt>mailto:</tt>, a policy may specify an <tt>https:</tt>
 destination. </p>
 
-<p> The diagram below shows how Postfix TLS handshake success and
-failure events are collected and processed into daily summary
+<p> The diagram below shows how successful or failed Postfix TLS
+handshake events are collected and processed into daily summary
 reports. </p>
 
 <blockquote>
@@ -203,6 +204,49 @@ programs should create sockets there. </p>
 infrastructure, see the documentation at
 <a href="https://github.com/sys4/tlsrpt-reporter">https://github.com/sys4/tlsrpt-reporter</a>.
 
+<h2> <a name="reusing"> Connection reuse versus session resumption
+</a> </h2>
+
+<p> The Postfix SMTP client implements two kinds of reuse: </p>
+
+<ul>
+
+<li> <p> <b> SMTP Connection reuse: </b> a Postfix SMTP client
+creates a new SMTP connection, sends one email message, and saves
+the connection instead of closing it. Later, some SMTP client reuses
+that connection, sends an email message, and saves or closes the
+connection depending on whether it has reached some reuse limit.
+Each connection can be used by only one Postfix SMTP client at a
+time. </p>
+
+<li> <p> <b> TLS Session resumption: </b> a Postfix SMTP client
+saves the result from a "new" TLS handshake. Later, one or more
+SMTP clients create a new SMTP connection and resume the saved TLS
+session on their new connection. <p>
+
+</ul>
+
+<p> Of course there is a third case: </p>
+
+<ul>
+
+<li> <p> <b> Combined reuse and resumption: </b> a Postfix SMTP
+client creates a new SMTP connection, sends one email message, saves
+the result from a "new" TLS handshake, and also saves the connection
+instead of closing it. Later, one SMTP client reuses (and saves)
+that connection, one client at a time, and one or more clients
+create a new SMTP connection and resume the saved TLS session on
+their new connection. <p>
+
+</ul>
+
+<p> In all cases, there is no TLS handshake when a saved SMTP connection
+is reused, and there is no "new" TLS handshake when a saved TLS session
+is resumed. </p>
+
+<p> As described next, Postfix will by default log and generate only a
+TLSRPT event for a "new" TLS handshake.  </p>
+
 <h2> <a name="logging"> TLSRPT Status logging </a> </h2>
 
 <p> With TLSRPT support turned on, the Postfix TLSRPT client will
index 7778a673cad8e04c08eb328bdb4b111099a4bf06..0531b0381ae5652983e9d6bbe8355bb78d2bb445 100644 (file)
@@ -25,6 +25,7 @@
 <li> <a href="#intro"> Introduction </a> </li>
 <li> <a href="#building"> Building Postfix with TLSRPT support </a>
 <li> <a href="#using"> Turning on TLSRPT </a> </li>
+<li> <a href="#reusing"> Connection reuse versus session resumption </a> </li>
 <li> <a href="#logging"> TLSRPT Status logging </a> </li>
 <li> <a href="#delivering"> Delivering TLSRPT summaries via email</a> </li>
 <li> <a href="#mta-sts"> MTA-STS Support via smtp_tls_policy_maps </a> </li>
@@ -52,8 +53,8 @@ _smtp._tls.example.com. IN TXT "v=TLSRPTv1; rua=mailto:smtp-tls-report@example.c
 <p> Instead of <tt>mailto:</tt>, a policy may specify an <tt>https:</tt>
 destination. </p>
 
-<p> The diagram below shows how Postfix TLS handshake success and
-failure events are collected and processed into daily summary
+<p> The diagram below shows how successful or failed Postfix TLS
+handshake events are collected and processed into daily summary
 reports. </p>
 
 <blockquote>
@@ -203,6 +204,49 @@ programs should create sockets there. </p>
 infrastructure, see the documentation at
 https://github.com/sys4/tlsrpt-reporter.
 
+<h2> <a name="reusing"> Connection reuse versus session resumption
+</a> </h2>
+
+<p> The Postfix SMTP client implements two kinds of reuse: </p>
+
+<ul>
+
+<li> <p> <b> SMTP Connection reuse: </b> a Postfix SMTP client
+creates a new SMTP connection, sends one email message, and saves
+the connection instead of closing it. Later, some SMTP client reuses
+that connection, sends an email message, and saves or closes the
+connection depending on whether it has reached some reuse limit.
+Each connection can be used by only one Postfix SMTP client at a
+time. </p>
+
+<li> <p> <b> TLS Session resumption: </b> a Postfix SMTP client
+saves the result from a "new" TLS handshake. Later, one or more
+SMTP clients create a new SMTP connection and resume the saved TLS
+session on their new connection. <p>
+
+</ul>
+
+<p> Of course there is a third case: </p>
+
+<ul>
+
+<li> <p> <b> Combined reuse and resumption: </b> a Postfix SMTP
+client creates a new SMTP connection, sends one email message, saves
+the result from a "new" TLS handshake, and also saves the connection
+instead of closing it. Later, one SMTP client reuses (and saves)
+that connection, one client at a time, and one or more clients
+create a new SMTP connection and resume the saved TLS session on
+their new connection. <p>
+
+</ul>
+
+<p> In all cases, there is no TLS handshake when a saved SMTP connection
+is reused, and there is no "new" TLS handshake when a saved TLS session
+is resumed. </p>
+
+<p> As described next, Postfix will by default log and generate only a
+TLSRPT event for a "new" TLS handshake.  </p>
+
 <h2> <a name="logging"> TLSRPT Status logging </a> </h2>
 
 <p> With TLSRPT support turned on, the Postfix TLSRPT client will
index 1b85ee76800d4ca8160ccf1dd088dc8aac686ea4..1a66bdb7f69fc212fb5c39579b9ee6d10b5ce7e1 100644 (file)
@@ -104,3 +104,4 @@ Gueven
 Oemer
 Kozmenko
 Oleksandr
+Bataille
index 67520c7e2fb85d2cda3e49a77a40c9e22aa7ec00..6ed62d412af687d3e10fe3cc5758237aa76b6c89 100644 (file)
@@ -131,7 +131,7 @@ TESTPROG= domain_list dot_lockfile mail_addr_crunch mail_addr_find \
        fold_addr smtp_reply_footer mail_addr_map normalize_mailhost_addr \
        haproxy_srvr map_search delivered_hdr login_sender_match \
        compat_level config_known_tcp_ports hfrom_format rfc2047_code \
-       ascii_header_text sendopts_test
+       ascii_header_text sendopts_test dict_sqlite_test
 
 LIBS   = ../../lib/lib$(LIB_PREFIX)util$(LIB_SUFFIX)
 LIB_DIR        = ../../lib
@@ -408,6 +408,10 @@ ascii_header_text: ascii_header_text.c $(LIB) $(LIBS)
 sendopts_test: sendopts_test.c $(LIB) $(LIBS)
        $(CC) $(CFLAGS) -DTEST -o $@ $@.c $(LIB) $(LIBS) $(SYSLIBS)
 
+dict_sqlite_test: dict_sqlite_test.c dict_sqlite.o $(LIB) $(LIBS)
+       $(CC) $(CFLAGS) -DTEST -o $@ $@.c dict_sqlite.o $(LIB) $(LIBS) \
+           $(SYSLIBS) $(AUXLIBS_SQLITE)
+
 config_known_tcp_ports: config_known_tcp_ports.c $(LIB) $(LIBS)
        $(CC) $(CFLAGS) -DTEST -o $@ $@.c $(LIB) $(LIBS) $(SYSLIBS)
 
@@ -421,7 +425,7 @@ tests: tok822_test mime_tests strip_addr_test tok822_limit_test \
        normalize_mailhost_addr_test haproxy_srvr_test map_search_test \
        delivered_hdr_test login_sender_match_test compat_level_test \
        config_known_tcp_ports_test hfrom_format_test rfc2047_code_test \
-       ascii_header_text_test test_sendopts
+       ascii_header_text_test test_sendopts test_dict_sqlite
 
 mime_tests: mime_test mime_nest mime_8bit mime_dom mime_trunc mime_cvt \
        mime_cvt2 mime_cvt3 mime_garb1 mime_garb2 mime_garb3 mime_garb4
@@ -794,7 +798,10 @@ ascii_header_text_test: update ascii_header_text
        $(SHLIB_ENV) $(VALGRIND) ./ascii_header_text
 
 test_sendopts: update sendopts_test
-       -$(SHLIB_ENV) $(VALGRIND) ./sendopts_test
+       $(SHLIB_ENV) $(VALGRIND) ./sendopts_test
+
+test_dict_sqlite: update dict_sqlite_test
+       $(SHLIB_ENV) $(VALGRIND) ./dict_sqlite_test
 
 clean:
        rm -f *.o $(LIB) *core $(TESTPROG) junk $(MAPS)
@@ -1331,6 +1338,19 @@ dict_sqlite.o: db_common.h
 dict_sqlite.o: dict_sqlite.c
 dict_sqlite.o: dict_sqlite.h
 dict_sqlite.o: string_list.h
+dict_sqlite_test.o: ../../include/argv.h
+dict_sqlite_test.o: ../../include/check_arg.h
+dict_sqlite_test.o: ../../include/dict.h
+dict_sqlite_test.o: ../../include/msg.h
+dict_sqlite_test.o: ../../include/msg_vstream.h
+dict_sqlite_test.o: ../../include/myflock.h
+dict_sqlite_test.o: ../../include/stringops.h
+dict_sqlite_test.o: ../../include/sys_defs.h
+dict_sqlite_test.o: ../../include/vbuf.h
+dict_sqlite_test.o: ../../include/vstream.h
+dict_sqlite_test.o: ../../include/vstring.h
+dict_sqlite_test.o: dict_sqlite.h
+dict_sqlite_test.o: dict_sqlite_test.c
 domain_list.o: ../../include/argv.h
 domain_list.o: ../../include/check_arg.h
 domain_list.o: ../../include/match_list.h
index c267ce4c67f95073d33101da38ee40c876ff0115..c8881f368124961ca8bc87121150663119807a09 100644 (file)
 /*     Must be O_RDONLY.
 /* .IP dict_flags
 /*     See dict_open(3).
+/* DIAGNOSTICS
+/*     dict_sqlite_open() logs a warning when the query parameter value
+/*     does not use the recommended '' quotes to protect against SQL
+/*     injection (bad examples; no quotes or "" quotes).
 /* SEE ALSO
 /*     dict(3) generic dictionary manager
-/*     sqlite_table(5) sqlite client configuration
+/*     sqlite_table(5) Postfix sqlite client configuration
 /* AUTHOR(S)
 /*     Axel Steiner
 /*     ast@treibsand.com
 /*     IBM T.J. Watson Research
 /*     P.O. Box 704
 /*     Yorktown Heights, NY 10598, USA
+/*
+/*     Wietse Venema
+/*     porcupine.org
 /*--*/
 
 /* System library. */
 
 #include <sys_defs.h>
 #include <string.h>
+#include <ctype.h>
 
 #ifdef HAS_SQLITE
 #include <sqlite3.h>
@@ -250,6 +258,38 @@ static const char *dict_sqlite_lookup(DICT *dict, const char *name)
            retval : 0);
 }
 
+/* flag_non_recommended_query - as the name says. */
+
+static void flag_non_recommended_query(const char *query,
+                                              const char *sqlitecf)
+{
+    const char *cp;
+    int     in_quote;
+    const int squote = '\'';
+    const int dquote = '"';
+
+    for (in_quote = 0, cp = query; *cp != 0; cp++) {
+       if (in_quote == 0) {
+           if (*cp == squote || *cp == dquote)
+               in_quote = *cp;
+       } else if (*cp == in_quote) {
+           in_quote = 0;
+       }
+       if (in_quote == squote)
+           continue;
+       if (*cp == '%') {
+           if (cp[1] == '%') {
+               cp += 1;
+           } else if (ISALNUM(cp[1])) {
+               msg_warn("%s:%s: query >%s< contains >%.2s< without the "
+                        "recommended '' quotes", DICT_TYPE_SQLITE, sqlitecf,
+                        query, cp);
+               cp += 1;
+           }
+       }
+    }
+}
+
 /* sqlite_parse_config - parse sqlite configuration file */
 
 static void sqlite_parse_config(DICT_SQLITE *dict_sqlite, const char *sqlitecf)
@@ -268,6 +308,8 @@ static void sqlite_parse_config(DICT_SQLITE *dict_sqlite, const char *sqlitecf)
        db_common_sql_build_query(buf, dict_sqlite->parser);
        dict_sqlite->query = vstring_export(buf);
     }
+    /* Flag %[a-zA-Z0-9] if not protected with ''. */
+    flag_non_recommended_query(dict_sqlite->query, sqlitecf);
     dict_sqlite->result_format =
        cfg_get_str(dict_sqlite->parser, "result_format", "%s", 1, 0);
     dict_sqlite->expansion_limit =
index fb2bdf25fd922fd4d29ca528919f9139f499969d..1c0e8b6a650aa396aef8965fac44947b00957159 100644 (file)
@@ -23,7 +23,6 @@
 
 extern DICT *dict_sqlite_open(const char *, int, int);
 
-
 /* AUTHOR(S)
 /*     Axel Steiner
 /*     ast@treibsand.com
diff --git a/postfix/src/global/dict_sqlite_test.c b/postfix/src/global/dict_sqlite_test.c
new file mode 100644 (file)
index 0000000..e3cce1e
--- /dev/null
@@ -0,0 +1,247 @@
+/*++
+/* NAME
+/*     dict_sqlite_test 1t
+/* SUMMARY
+/*     dict_sqlite unit test
+/* SYNOPSIS
+/*     ./dict_sqlite_test
+/* DESCRIPTION
+/*     dict_sqlite_test runs and logs each configured test, reports if
+/*     a test is a PASS or FAIL, and returns an exit status of zero if
+/*     all tests are a PASS.
+/*
+/*     Each test creates a temporary test database and a corresponding
+/*     Postfix sqlite client configuration file, both having unique
+/*     names. Otherwise, each test is hermetic.
+/* LICENSE
+/* .ad
+/* .fi
+/*     The Secure Mailer license must be distributed with this software.
+/* AUTHOR(S)
+/*     Wietse Venema porcupine.org
+/*--*/
+
+/*     
+ /*
+  * System library.
+  */
+#include <sys_defs.h>
+#include <stdlib.h>
+#include <string.h>
+
+ /*
+  * Utility library.
+  */
+#include <msg.h>
+#include <msg_vstream.h>
+#include <stringops.h>
+
+ /*
+  * Global library.
+  */
+#include <dict_sqlite.h>
+
+ /*
+  * TODO(wietse) make this a proper VSTREAM interface or test helper API.
+  */
+
+/* vstream_swap - capture output for testing */
+
+static void vstream_swap(VSTREAM *one, VSTREAM *two)
+{
+    VSTREAM save;
+
+    save = *one;
+    *one = *two;
+    *two = save;
+}
+
+ /*
+  * Override the printable.c module because it may break some tests.
+  * 
+  * TODO(wietse) move this to a fake_printable.c module that can override all
+  * printable.c global symbols.
+  */
+int     util_utf8_enable;
+
+char   *printable_except(char *string, int replacement, const char *except)
+{
+    return (string);
+}
+
+ /*
+  * Scaffolding for dict_sqlite(3) tests.
+  */
+
+/* create_and_populate_db - create an empty database and optionally populate */
+
+static void create_and_populate_db(char *dbpath, const char *commands)
+{
+    int     fd;
+
+    /*
+     * Create an empty database file with a unique name. Assume that an
+     * adversary cannot rename or remove the file.
+     */
+    if ((fd = mkstemp(dbpath)) < 0)
+       msg_fatal("mkstemp(\"%s\"): %m", dbpath);
+    if (close(fd) < 0)
+       msg_fatal("close %s: %m", dbpath);
+
+    /*
+     * TODO(wietse) Open the database file, prepare and execute commands
+     * to populate the database, and close the database.
+     */
+    if (commands) {
+       msg_fatal("commands are not yet supported");
+    }
+}
+
+/* create_and_populate_cf - create sqlite_table(5) configuration file */
+
+static void create_and_populate_cf(char *cfpath, const char *dbpath,
+                                         const char *cftext)
+{
+    int     fd;
+    VSTREAM *fp;
+
+    /*
+     * Create an empty sqlite_table(5) configuration file with a unique name.
+     * Assume that an adversary cannot rename or remove the file.
+     */
+    if ((fd = mkstemp(cfpath)) < 0)
+       msg_fatal("mkstemp(\"%s\"): %m", cfpath);
+    if ((fp = vstream_fdopen(fd, O_WRONLY)) == 0)
+       msg_fatal("vstream_fdopen: %m");
+    (void) vstream_fprintf(fp, "%s\ndbpath = %s\n", cftext, dbpath);
+    if (vstream_fclose(fp) != 0)
+       msg_fatal("vstream_fdclose: %m");
+}
+
+ /*
+  * Test structure. Some tests may come their own.
+  */
+typedef struct TEST_CASE {
+    const char *label;
+    int     (*action) (const struct TEST_CASE *);
+    const char *commands;              /* commands or null */
+    const char *settings;              /* sqlite_table(5) */
+    const char *exp_warning;           /* substring match or null */
+} TEST_CASE;
+
+#define PASS    (0)
+#define FAIL    (1)
+
+#define PATH_TEMPLATE  "/tmp/test-XXXXXXX"
+
+/* test_flag_non_recommended_query - flag non-recommended query payloads */
+
+static int test_flag_non_recommended_query(const TEST_CASE *tp)
+{
+    static VSTRING *msg_buf;
+    VSTREAM *memory_stream;
+    const char template[] = PATH_TEMPLATE;
+    char    dbpath[sizeof(template)];
+    char    cfpath[sizeof(template)];
+    DICT   *dict;
+
+    if (msg_buf == 0)
+       msg_buf = vstring_alloc(100);
+
+    /* Prepare scaffolding database and configuration files. */
+    memcpy(dbpath, template, sizeof(dbpath));
+    create_and_populate_db(dbpath, tp->commands);
+    memcpy(cfpath, template, sizeof(cfpath));
+    create_and_populate_cf(cfpath, dbpath, tp->settings);
+
+    /* Run the test with custom STDERR stream. */
+    VSTRING_RESET(msg_buf);
+    VSTRING_TERMINATE(msg_buf);
+    if ((memory_stream = vstream_memopen(msg_buf, O_WRONLY)) == 0)
+       msg_fatal("open memory stream: %m");
+    vstream_swap(VSTREAM_ERR, memory_stream);
+    if ((dict = dict_sqlite_open(cfpath, O_RDONLY, DICT_FLAG_UTF8_REQUEST)) != 0)
+       dict_close(dict);
+    vstream_swap(memory_stream, VSTREAM_ERR);
+    if (vstream_fclose(memory_stream))
+       msg_fatal("close memory stream: %m");
+
+    /* Cleanup scaffolding database and configuration files. */
+    if (unlink(dbpath) < 0)
+       msg_fatal("unlink %s: %m", dbpath);
+    if (unlink(cfpath) < 0)
+       msg_fatal("unlink %s: %m", cfpath);
+
+    /* Verify the results. */
+    if (tp->exp_warning == 0 && VSTRING_LEN(msg_buf) > 0) {
+       msg_warn("got warning ``%s'', want ``null''", vstring_str(msg_buf));
+       return (FAIL);
+    }
+    if (tp->exp_warning != 0
+       && strstr(vstring_str(msg_buf), tp->exp_warning) == 0) {
+       msg_warn("got warning ``%s'', want ``%s''",
+                vstring_str(msg_buf), tp->exp_warning);
+       return (FAIL);
+    }
+    return (PASS);
+}
+
+ /*
+  * The list of test cases.
+  */
+static const TEST_CASE test_cases[] = {
+
+    /*
+     * Tests to flag non-recommended query forms. These create an empty test
+     * database, and open it with the dict_sqlite client without querying it.
+     */
+    {.label = "no_dynamic_payload",
+       .action = test_flag_non_recommended_query,
+       .settings = "query = select a from b where c = 5",
+    },
+    {.label = "dynamic_payload_inside_recommended_quotes",
+       .action = test_flag_non_recommended_query,
+       .settings = "query = select a from b where c = 'xx%syy'",
+    },
+    {.label = "dynamic_payload_without_quotes",
+       .action = test_flag_non_recommended_query,
+       .settings = "query = select s from b where c = xx%syy",
+       .exp_warning = "contains >%s< without the recommended '' quotes",
+    },
+    {.label = "payload_inside_double_quotes",
+       .action = test_flag_non_recommended_query,
+       .settings = "query = select s from b where c = \"xx%syy\"",
+       .exp_warning = "contains >%s< without the recommended '' quotes",
+    },
+
+    /*
+     * TODO: Tests that actually populate a test database, and that query it
+     * with the dict_sqlite client.
+     */
+    {0},
+};
+
+int     main(int argc, char **argv)
+{
+    const TEST_CASE *tp;
+    int     pass = 0;
+    int     fail = 0;
+
+    msg_vstream_init(sane_basename((VSTRING *) 0, argv[0]), VSTREAM_ERR);
+
+    for (tp = test_cases; tp->label != 0; tp++) {
+       int     test_failed;
+
+       msg_info("RUN  %s", tp->label);
+       test_failed = tp->action(tp);
+       if (test_failed) {
+           msg_info("FAIL %s", tp->label);
+           fail++;
+       } else {
+           msg_info("PASS %s", tp->label);
+           pass++;
+       }
+    }
+    msg_info("PASS=%d FAIL=%d", pass, fail);
+    exit(fail != 0);
+}
index 5996ab3309644db188b0fbb465b8605d6084c7ff..87fd6ced41c5882144bc33eb618bdc650d0e782d 100644 (file)
@@ -20,7 +20,7 @@
   * Patches change both the patchlevel and the release date. Snapshots have no
   * patchlevel; they change the release date only.
   */
-#define MAIL_RELEASE_DATE      "20250304"
+#define MAIL_RELEASE_DATE      "20250323"
 #define MAIL_VERSION_NUMBER    "3.11"
 
 #ifdef SNAPSHOT
index 7085acd7278d406341253a768e26b199158d7fdf..437f2a87013ae379ec90d275fe1e23e5dbf70dde 100644 (file)
@@ -113,8 +113,13 @@ static char *pcf_find_cf_info(VSTRING *buf, VSTREAM *dst)
 static char *pcf_next_cf_line(VSTRING *buf, VSTREAM *src, VSTREAM *dst, int *lineno)
 {
     char   *cp;
+    int     last_char;
 
-    while (vstring_get(buf, src) != VSTREAM_EOF) {
+    while ((last_char = vstring_get(buf, src)) != VSTREAM_EOF) {
+       if (last_char != '\n') {
+           VSTRING_ADDCH(buf, '\n');
+           VSTRING_TERMINATE(buf);
+       }
        if (lineno)
            *lineno += 1;
        if ((cp = pcf_find_cf_info(buf, dst)) != 0)
index 451cc501784e9ebb46a045035b046939e299bcf2..3bf5e050f6c4475853d837985292cf570c545bc9 100644 (file)
  /*
   * Macro to return the last character added to a VSTRING, for consistency.
   */
-#define VSTRING_GET_RESULT(vp, baselen) \
+#define VSTRING_GET_RESULT(vp, base_len) \
     (VSTRING_LEN(vp) > (base_len) ? vstring_end(vp)[-1] : VSTREAM_EOF)
 
 /* vstring_get_flags - read line from file, keep newline */
@@ -142,7 +142,7 @@ int     vstring_get_flags(VSTRING *vp, VSTREAM *fp, int flags)
            break;
     }
     VSTRING_TERMINATE(vp);
-    return (VSTRING_GET_RESULT(vp, baselen));
+    return (VSTRING_GET_RESULT(vp, base_len));
 }
 
 /* vstring_get_flags_nonl - read line from file, strip newline */
@@ -158,7 +158,7 @@ int     vstring_get_flags_nonl(VSTRING *vp, VSTREAM *fp, int flags)
     while ((c = VSTREAM_GETC(fp)) != VSTREAM_EOF && c != '\n')
        VSTRING_ADDCH(vp, c);
     VSTRING_TERMINATE(vp);
-    return (c == '\n' ? c : VSTRING_GET_RESULT(vp, baselen));
+    return (c == '\n' ? c : VSTRING_GET_RESULT(vp, base_len));
 }
 
 /* vstring_get_flags_null - read null-terminated string from file */
@@ -174,7 +174,7 @@ int     vstring_get_flags_null(VSTRING *vp, VSTREAM *fp, int flags)
     while ((c = VSTREAM_GETC(fp)) != VSTREAM_EOF && c != 0)
        VSTRING_ADDCH(vp, c);
     VSTRING_TERMINATE(vp);
-    return (c == 0 ? c : VSTRING_GET_RESULT(vp, baselen));
+    return (c == 0 ? c : VSTRING_GET_RESULT(vp, base_len));
 }
 
 /* vstring_get_flags_bound - read line from file, keep newline, up to bound */
@@ -197,7 +197,7 @@ int     vstring_get_flags_bound(VSTRING *vp, VSTREAM *fp, int flags,
            break;
     }
     VSTRING_TERMINATE(vp);
-    return (VSTRING_GET_RESULT(vp, baselen));
+    return (VSTRING_GET_RESULT(vp, base_len));
 }
 
 /* vstring_get_flags_nonl_bound - read line from file, strip newline, up to bound */
@@ -217,7 +217,7 @@ int     vstring_get_flags_nonl_bound(VSTRING *vp, VSTREAM *fp, int flags,
     while (bound-- > 0 && (c = VSTREAM_GETC(fp)) != VSTREAM_EOF && c != '\n')
        VSTRING_ADDCH(vp, c);
     VSTRING_TERMINATE(vp);
-    return (c == '\n' ? c : VSTRING_GET_RESULT(vp, baselen));
+    return (c == '\n' ? c : VSTRING_GET_RESULT(vp, base_len));
 }
 
 /* vstring_get_flags_null_bound - read null-terminated string from file */
@@ -237,7 +237,7 @@ int     vstring_get_flags_null_bound(VSTRING *vp, VSTREAM *fp, int flags,
     while (bound-- > 0 && (c = VSTREAM_GETC(fp)) != VSTREAM_EOF && c != 0)
        VSTRING_ADDCH(vp, c);
     VSTRING_TERMINATE(vp);
-    return (c == 0 ? c : VSTRING_GET_RESULT(vp, baselen));
+    return (c == 0 ? c : VSTRING_GET_RESULT(vp, base_len));
 }
 
 #ifdef TEST