]> git.ipfire.org Git - thirdparty/postfix.git/commitdiff
postfix-3.9-20240124
authorWietse Venema <wietse@porcupine.org>
Wed, 24 Jan 2024 05:00:00 +0000 (00:00 -0500)
committerViktor Dukhovni <ietf-dane@dukhovni.org>
Thu, 25 Jan 2024 03:32:27 +0000 (22:32 -0500)
17 files changed:
postfix/HISTORY
postfix/Makefile.in
postfix/html/postconf.5.html
postfix/man/man5/postconf.5
postfix/mantools/check-snapshot-nonprod [new file with mode: 0755]
postfix/proto/postconf.proto
postfix/proto/stop.double-history
postfix/proto/stop.double-proto-html
postfix/proto/stop.spell-history
postfix/src/global/mail_version.h
postfix/src/smtpd/smtpd.c
postfix/src/smtpd/smtpd.h
postfix/src/smtpd/smtpd_state.c
postfix/src/tlsmgr/tlsmgr.c
postfix/src/util/Makefile.in
postfix/src/util/argv.c
postfix/src/util/argv.h

index 1c7569cb4d0a0990ffab8010622174633ad22564..76ae1c079c870980f6cc44b31f69a0a67712e213 100644 (file)
@@ -27732,3 +27732,31 @@ Apologies for any names omitted.
 
        Baseline for back porting the SMTP smuggling fixes to Postfix
        3.8.5, 3.7.10, 3.6.14, and 3.5.24.
+
+20240124
+
+       Feature: with "smtpd_forbid_bare_newline = note", the Postfix
+       SMTP server notes in the log if it received any lines with
+       bare LF.  Otherwise, "note" is like "normalize". The
+       information is formatted as "disconnect from name[address]
+       ...  notes=bare_lf".  The new value is expected to become
+       a list of comma-separated names. Files: smtpd/smtpd.[hc].
+
+       Cleanup: require that a stable release disables SNAPSHOT
+       and NONPROD features. File: mantools/check-snapshot-nonprod.
+
+       Bugfix (defect introduced: Postfix 3.4): the SMTP server's
+       BDAT command handler could be tricked to read $message_size_limit
+       bytes into memory. Found during code maintenance. File:
+       smtpd/smtpd.c.
+
+       Feature: never too late, an SMTP server HELP command that
+       lists the implemented commands. Some commands may be
+       implemented but not available due to smtpd_discard_ehlo_keywords
+       or access control limitations. Files: smtpd/smtpd.[hc],
+       util/argv.[hc].
+
+       Workaround: some OS lies under load: it says that a socket
+       is readable, then it says that the socket has unread data,
+       and then it says that read returns EOF, causing Postfix to
+       spam the log with a warning message. File: tlsmgr/tlsmgr.c.
index 54c2aa22df5a31278c9775c9c5803e99f04cd53f..4b1fb33ecfd2934f783779edd509e10de583e0d0 100644 (file)
@@ -117,7 +117,8 @@ manpages:
 # Some checks require a bin/postconf executable.
 pre-release-checks: typo-check missing-proxy-read-maps-check \
        postlink-check postfix-files-check check-spell-history \
-       check-double-history check-table-proto check-see-postconf-d-output
+       check-double-history check-table-proto check-see-postconf-d-output \
+       check-snapshot-nonprod
 
 postfix-files-check:
        mantools/check-postfix-files | diff /dev/null -
@@ -161,6 +162,9 @@ check-table-proto:
 check-see-postconf-d-output:
        mantools/check-see-postconf-d-output | diff /dev/null -
 
+check-snapshot-nonprod:
+       mantools/check-snapshot-nonprod
+
 # The build-time shlib_directory setting must take precedence over
 # the installed main.cf settings, otherwise we can't update an
 # installed system from dynamicmaps=yes<->dynamicmaps=no or from
index 8252c6f2b7b11a4369ad8d3ab92a4a400db38fdf..d0632412eef623969c01be05a3e4c665c7e32f99 100644 (file)
@@ -15982,6 +15982,13 @@ with the standard End-of-DATA sequence
 &lt;CR&gt;&lt;LF&gt;.&lt;CR&gt;&lt;LF&gt;. <br> <br> Such clients
 can be excluded with <a href="postconf.5.html#smtpd_forbid_bare_newline_exclusions">smtpd_forbid_bare_newline_exclusions</a>. </dd>
 
+<dt> <b>note</b> </dt> <dd> Same as "normalize", but also notes in
+the log whether the Postfix SMTP server received any lines with
+"bare &lt;LF&gt;". The information is formatted as "<tt>disconnect
+from name[address] ...  notes=bare_lf</tt>". The new value is
+expected to become a list of comma-separated names. <br> <br> This
+feature is available in Postfix 3.9 and later. </dd>
+
 <dt> <b>yes</b> </dt> <dd> Compatibility alias for <b>normalize</b>. </dd>
 
 <dt> <b>reject</b> </dt> <dd> Require the standard End-of-DATA
index 9c1582dbcfbf7cbf55bb132f26050d0b6739ebe6..32e4a8ee70d1a73801b4c69215489e23238b0f37 100644 (file)
@@ -11057,6 +11057,17 @@ with the standard End\-of\-DATA sequence
 Such clients
 can be excluded with smtpd_forbid_bare_newline_exclusions.
 .br
+.IP "\fBnote\fR"
+Same as "normalize", but also notes in
+the log whether the Postfix SMTP server received any lines with
+"bare <LF>". The information is formatted as "disconnect
+from name[address] ...  notes=bare_lf". The new value is
+expected to become a list of comma\-separated names.
+.br
+.br
+This
+feature is available in Postfix 3.9 and later.
+.br
 .IP "\fByes\fR"
 Compatibility alias for \fBnormalize\fR.
 .br
diff --git a/postfix/mantools/check-snapshot-nonprod b/postfix/mantools/check-snapshot-nonprod
new file mode 100755 (executable)
index 0000000..e18f6aa
--- /dev/null
@@ -0,0 +1,13 @@
+#!/bin/sh
+
+version=$(basename $(env - pwd)) || exit 1
+case "$version" in
+postfix-[0-9]*.[0-9]*.[0-9]*) 
+    test -f conf/makedefs.out || {
+       echo "Error: no conf/makedefs.out" 1>&2; exit 1; }
+    grep 'CCARGS.*-DSNAPSHOT' conf/makedefs.out && {
+       echo "Error: stable release builds with -DSNAPSHOT" 1>&2, exit 1; }
+    grep 'CCARGS.*-DNONPROD' conf/makedefs.out && {
+       echo "Error: stable release builds with -DNONPROD" 1>&2, exit 1; }
+    ;;
+esac
index bf2b185f5a527a798b0f47f06ce136b37f27f100..b4219e1a16b9bfddd820fb237988fca7606f022a 100644 (file)
@@ -19081,6 +19081,13 @@ with the standard End-of-DATA sequence
 &lt;CR&gt;&lt;LF&gt;.&lt;CR&gt;&lt;LF&gt;. <br> <br> Such clients
 can be excluded with smtpd_forbid_bare_newline_exclusions. </dd>
 
+<dt> <b>note</b> </dt> <dd> Same as "normalize", but also notes in
+the log whether the Postfix SMTP server received any lines with
+"bare &lt;LF&gt;". The information is formatted as "<tt>disconnect
+from name[address] ...  notes=bare_lf</tt>". The new value is
+expected to become a list of comma-separated names. <br> <br> This
+feature is available in Postfix 3.9 and later. </dd>
+
 <dt> <b>yes</b> </dt> <dd> Compatibility alias for <b>normalize</b>. </dd>
 
 <dt> <b>reject</b> </dt> <dd> Require the standard End-of-DATA
index d4a1183da91b589aff243ade55cbeedbb7c7abb7..8260b740ea7e55c52ef6e83348246137b807b13e 100644 (file)
@@ -97,3 +97,6 @@ proto  proto aliases proto virtual proto ADDRESS_REWRITING_README html
  stable releases Files global smtp_stream hc smtpd smtpd c
  Files global smtp_stream hc smtpd smtpd c 
  Files smtpd smtpd c proto postconf proto 
+ names Files smtpd smtpd hc 
+ or access control limitations Files smtpd smtpd hc 
+ spam the log with a warning message File tlsmgr tlsmgr c 
index 2e1962e68d6ca3daa61071eae6da5b673400ad05..e49fb4da212853e6048fd7dfa4ce000b33aceefd 100644 (file)
@@ -352,3 +352,4 @@ standard lt CR gt lt LF gt br br This maintains compatibility
 2045 Sections 2 7 and 2 8 br br Such clients can be excluded
  br br This will also reject email from services that use BDAT
 RFC 2045 Sections 2 7 and 2 8 br br Such clients can be
+to become a list of comma separated names br br This feature
index 912bffcecf9c193b1a8216976dcd9b2155a2d8e7..b688da179472558f25f29d126ff200bca068dd85 100644 (file)
@@ -67,3 +67,4 @@ Birta
 Levente
 MariaDB
 dehtml
+NONPROD
index 6a55838df65ab6c2ba0f8f52d579f3400448c6f1..82ffec7387a8662249d6852d9a0b8974acd91b80 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      "20240121"
+#define MAIL_RELEASE_DATE      "20240124"
 #define MAIL_VERSION_NUMBER    "3.9"
 
 #ifdef SNAPSHOT
index f7aeb8799c23e0196fe5a3575be2d8bd30c0a08b..bce0d43f69952a01f1baa53a3df7bacd8cb726b6 100644 (file)
@@ -1672,13 +1672,16 @@ int     smtpd_hfrom_format;
   */
 #define BARE_LF_FLAG_WANT_STD_EOD      (1<<0)  /* Require CRLF.CRLF */
 #define BARE_LF_FLAG_REPLY_REJECT      (1<<1)  /* Reject bare newline */
+#define BARE_LF_FLAG_NOTE_LOG          (1<<2)  /* Note bare newline */
 
 #define IS_BARE_LF_WANT_STD_EOD(m)     ((m) & BARE_LF_FLAG_WANT_STD_EOD)
 #define IS_BARE_LF_REPLY_REJECT(m)     ((m) & BARE_LF_FLAG_REPLY_REJECT)
+#define IS_BARE_LF_NOTE_LOG(m)         ((m) & BARE_LF_FLAG_NOTE_LOG)
 
 static const NAME_CODE bare_lf_mask_table[] = {
     "normalize", BARE_LF_FLAG_WANT_STD_EOD,    /* Default */
     "yes", BARE_LF_FLAG_WANT_STD_EOD,  /* Migration aid */
+    "note", BARE_LF_FLAG_WANT_STD_EOD | BARE_LF_FLAG_NOTE_LOG,
     "reject", BARE_LF_FLAG_WANT_STD_EOD | BARE_LF_FLAG_REPLY_REJECT,
     "no", 0,
     0, -1,                             /* error */
@@ -3669,6 +3672,8 @@ static void receive_data_message(SMTPD_STATE *state,
            curr_rec_type = REC_TYPE_CONT;
        if (IS_BARE_LF_REPLY_REJECT(smtp_got_bare_lf))
            state->err |= CLEANUP_STAT_BARE_LF;
+       else if (IS_BARE_LF_NOTE_LOG(smtp_got_bare_lf))
+           state->notes |= SMTPD_NOTE_BARE_LF;
        start = vstring_str(state->buffer);
        len = VSTRING_LEN(state->buffer);
        if (first) {
@@ -4150,14 +4155,31 @@ static int bdat_cmd(SMTPD_STATE *state, int argc, SMTPD_TOKEN *argv)
        /*
         * Read lines from the fragment. The last line may continue in the
         * next fragment, or in the next chunk.
+        * 
+        * If smtp_get_noexcept() stopped after var_line_limit bytes and did not
+        * emit a queue file record, then that means smtp_get_noexcept()
+        * stopped after CR and hit EOF as it tried to find out if the next
+        * byte is LF. In that case, read the first byte from the next
+        * fragment or chunk, and if that first byte is LF, then
+        * smtp_get_noexcept() strips off the trailing CRLF and returns '\n'
+        * as it always does after reading a complete line.
         */
        do {
+           int     can_read = var_line_limit - LEN(state->bdat_get_buffer);
+
            if (smtp_get_noexcept(state->bdat_get_buffer,
                                  state->bdat_get_stream,
-                                 var_line_limit,
+                                 can_read > 0 ? can_read : 1,  /* Peek one */
                                  SMTP_GET_FLAG_APPEND) == '\n') {
                /* Stopped at end-of-line. */
                curr_rec_type = REC_TYPE_NORM;
+           } else if (LEN(state->bdat_get_buffer) > var_line_limit) {
+               /* Undo peeking, and output the buffer as REC_TYPE_CONT. */
+               vstream_ungetc(state->bdat_get_stream,
+                              vstring_end(state->bdat_get_buffer)[-1]);
+               vstring_truncate(state->bdat_get_buffer,
+                                LEN(state->bdat_get_buffer) - 1);
+               curr_rec_type = REC_TYPE_CONT;
            } else if (!vstream_feof(state->bdat_get_stream)) {
                /* Stopped at var_line_limit. */
                curr_rec_type = REC_TYPE_CONT;
@@ -4172,6 +4194,8 @@ static int bdat_cmd(SMTPD_STATE *state, int argc, SMTPD_TOKEN *argv)
            }
            if (IS_BARE_LF_REPLY_REJECT(smtp_got_bare_lf))
                state->err |= CLEANUP_STAT_BARE_LF;
+           else if (IS_BARE_LF_NOTE_LOG(smtp_got_bare_lf))
+               state->notes |= SMTPD_NOTE_BARE_LF;
            start = vstring_str(state->bdat_get_buffer);
            len = VSTRING_LEN(state->bdat_get_buffer);
            if (state->err == CLEANUP_STAT_OK) {
@@ -5474,8 +5498,6 @@ static void tls_reset(SMTPD_STATE *state)
 
 #endif
 
-#if !defined(USE_TLS) || !defined(USE_SASL_AUTH)
-
 /* unimpl_cmd - dummy for functionality that is not compiled in */
 
 static int unimpl_cmd(SMTPD_STATE *state, int argc, SMTPD_TOKEN *unused_argv)
@@ -5492,8 +5514,6 @@ static int unimpl_cmd(SMTPD_STATE *state, int argc, SMTPD_TOKEN *unused_argv)
     return (-1);
 }
 
-#endif
-
  /*
   * The table of all SMTP commands that we know. Set the junk limit flag on
   * any command that can be repeated an arbitrary number of times without
@@ -5518,6 +5538,8 @@ typedef struct SMTPD_CMD {
 #define SMTPD_CMD_FLAG_PRE_TLS (1<<1)  /* allow before STARTTLS */
 #define SMTPD_CMD_FLAG_LAST    (1<<2)  /* last in PIPELINING command group */
 
+static int  help_cmd(SMTPD_STATE *, int, SMTPD_TOKEN *);
+
 static SMTPD_CMD smtpd_cmd_table[] = {
     {SMTPD_CMD_HELO, helo_cmd, SMTPD_CMD_FLAG_LIMIT | SMTPD_CMD_FLAG_PRE_TLS | SMTPD_CMD_FLAG_LAST,},
     {SMTPD_CMD_EHLO, ehlo_cmd, SMTPD_CMD_FLAG_LIMIT | SMTPD_CMD_FLAG_PRE_TLS | SMTPD_CMD_FLAG_LAST,},
@@ -5542,12 +5564,44 @@ static SMTPD_CMD smtpd_cmd_table[] = {
     {SMTPD_CMD_VRFY, vrfy_cmd, SMTPD_CMD_FLAG_LIMIT | SMTPD_CMD_FLAG_LAST,},
     {SMTPD_CMD_ETRN, etrn_cmd, SMTPD_CMD_FLAG_LIMIT,},
     {SMTPD_CMD_QUIT, quit_cmd, SMTPD_CMD_FLAG_PRE_TLS,},
+    {SMTPD_CMD_HELP, help_cmd, SMTPD_CMD_FLAG_PRE_TLS,},
     {0,},
 };
 
 static STRING_LIST *smtpd_noop_cmds;
 static STRING_LIST *smtpd_forbid_cmds;
 
+/* help_cmd - process HELP command */
+
+static int help_cmd(SMTPD_STATE *state, int unused_argc, SMTPD_TOKEN *unused_argv)
+{
+    ARGV   *argv = argv_alloc(sizeof(smtpd_cmd_table)
+                             / sizeof(*smtpd_cmd_table));
+    VSTRING *buf = vstring_alloc(100);
+    SMTPD_CMD *cmdp;
+
+    /*
+     * Return a list of implemented commands.
+     * 
+     * The HELP command does not suppress commands that can be dynamically
+     * disabled in the EHLO response or through access control. That would
+     * require refactoring the EHLO feature-suppression and per-feature
+     * access control, so that they can be reused (not duplicated) here.
+     * 
+     * The HELP command does not provide information that makes Postfix easier
+     * to fingerprint, such as software name, version, or build information.
+     */
+    for (cmdp = smtpd_cmd_table; cmdp->name != 0; cmdp++)
+       if (cmdp->action != unimpl_cmd)
+           argv_add(argv, cmdp->name, ARGV_END);
+    argv_sort(argv);
+    smtpd_chat_reply(state, "214 2.0.0 Commands: %s",
+                    argv_join(buf, argv, ' '));
+    vstring_free(buf);
+    argv_free(argv);
+    return (0);
+}
+
 /* smtpd_flag_ill_pipelining - flag pipelining protocol violation */
 
 static int smtpd_flag_ill_pipelining(SMTPD_STATE *state)
@@ -5874,6 +5928,8 @@ static void smtpd_proto(SMTPD_STATE *state)
                             var_smtpd_forbid_bare_lf_code, var_myhostname);
                break;
            }
+           if (IS_BARE_LF_NOTE_LOG(smtp_got_bare_lf))
+               state->notes |= SMTPD_NOTE_BARE_LF;
            /* Safety: protect internal interfaces against malformed UTF-8. */
            if (var_smtputf8_enable
                && valid_utf8_stringz(STR(state->buffer)) == 0) {
@@ -6060,11 +6116,12 @@ static void smtpd_proto(SMTPD_STATE *state)
 
 /* smtpd_format_cmd_stats - format per-command statistics */
 
-static char *smtpd_format_cmd_stats(VSTRING *buf)
+static char *smtpd_format_cmd_stats(SMTPD_STATE *state)
 {
     SMTPD_CMD *cmdp;
     int     all_success = 0;
     int     all_total = 0;
+    VSTRING *buf = state->buffer;
 
     /*
      * Log the statistics. Note that this loop produces no output when no
@@ -6108,6 +6165,13 @@ static char *smtpd_format_cmd_stats(VSTRING *buf)
     vstring_sprintf_append(buf, " commands=%d", all_success);
     if (all_success != all_total || all_total == 0)
        vstring_sprintf_append(buf, "/%d", all_total);
+
+    /*
+     * Log aggregated warnings.
+     */
+    if (state->notes & SMTPD_NOTE_BARE_LF)
+       vstring_sprintf_append(buf, " notes=bare_lf");
+
     return (lowercase(STR(buf)));
 }
 
@@ -6249,7 +6313,7 @@ static void smtpd_service(VSTREAM *stream, char *service, char **argv)
      * connection time.
      */
     msg_info("disconnect from %s%s", state.namaddr,
-            smtpd_format_cmd_stats(state.buffer));
+            smtpd_format_cmd_stats(&state));
     teardown_milters(&state);                  /* duplicates xclient_cmd */
     smtpd_state_reset(&state);
     debug_peer_restore();
index 56ebc078da0d0de861d09a30fb397afda3c5c873..c049194e2c31d99647b071a98107e7dae602664d 100644 (file)
@@ -114,6 +114,7 @@ typedef struct {
     int     junk_cmds;                 /* counter */
     int     rcpt_overshoot;            /* counter */
     char   *rewrite_context;           /* address rewriting context */
+    int     notes;                     /* notes aggregator */
 
     /*
      * SASL specific.
@@ -209,6 +210,8 @@ typedef struct {
 #define SMTPD_FLAG_SMTPUTF8       (1<<3)       /* RFC 6531/2 transaction */
 #define SMTPD_FLAG_NEED_MILTER_ABORT (1<<4)    /* undo milter_mail_event() */
 
+#define SMTPD_NOTE_BARE_LF        (1<<0)       /* saw at least one bare LF */
+
  /* Security: don't reset SMTPD_FLAG_AUTH_USED. */
 #define SMTPD_MASK_MAIL_KEEP \
            ~(SMTPD_FLAG_SMTPUTF8)      /* Fix 20140706 */
@@ -260,6 +263,7 @@ extern void smtpd_state_reset(SMTPD_STATE *);
 #define SMTPD_CMD_XCLIENT      "XCLIENT"
 #define SMTPD_CMD_XFORWARD     "XFORWARD"
 #define SMTPD_CMD_UNKNOWN      "UNKNOWN"
+#define SMTPD_CMD_HELP         "HELP"
 
  /*
   * Representation of unknown and non-existent client information. Throughout
index f2f5f899c68a429283260dbe5f0b6fffb0c2e077..fefc54362e64de5927f98e4417b1911dab53f843 100644 (file)
@@ -135,6 +135,7 @@ void    smtpd_state_init(SMTPD_STATE *state, VSTREAM *stream,
     state->instance = vstring_alloc(10);
     state->seqno = 0;
     state->rewrite_context = 0;
+    state->notes = 0;
 #if 0
     state->ehlo_discard_mask = ~0;
 #else
index 28ca96128efb2dd8cd300a563763952092cf8ca4..b586368360972cb3bcd187f7bc69a114ea768db7 100644 (file)
@@ -827,6 +827,21 @@ static void tlsmgr_service(VSTREAM *client_stream, char *unused_service,
                   ATTR_TYPE_END);
     }
     vstream_fflush(client_stream);
+
+    /*
+     * Reportedly, some OS lies under load; it tells the Postfix event
+     * handler that a server socket is readable, then it tells peekfd() that
+     * the socket has pending data, and then it tells vstring_get_null() that
+     * there is none, causing Postfix to spam the log with warning messages.
+     * Close the stream to stop such nonsense; the client can reconnect if it
+     * still wants to talk to us.
+     * 
+     * XXX Why is this problem not reported for the other five
+     * multi_server-based Postfix services?
+     */
+    if (vstream_ferror(client_stream) || vstream_feof(client_stream))
+       multi_server_disconnect(client_stream);
+    /* Note: client_stream is now a dangling pointer. */
 }
 
 /* tlsmgr_pre_init - pre-jail initialization */
index f4c8e6e5835113474a0977d090511c4a3e061bb0..5c31d5e1343eb596562be48535a8944f1f2f7171 100644 (file)
@@ -1139,9 +1139,12 @@ allspace.o: vbuf.h
 allspace.o: vstring.h
 argv.o: argv.c
 argv.o: argv.h
+argv.o: check_arg.h
 argv.o: msg.h
 argv.o: mymalloc.h
 argv.o: sys_defs.h
+argv.o: vbuf.h
+argv.o: vstring.h
 argv_attr_print.o: argv.h
 argv_attr_print.o: argv_attr.h
 argv_attr_print.o: argv_attr_print.c
index 4e05fd0b4b3ae9dcd1caa83efdf1bfb6215589f0..332426e88790ba9e51935207c2a7fc931cbd1e35 100644 (file)
 /*     ssize_t pos;
 /*     ssize_t how_many;
 /*
+/*     char    *argv_join(buf, argvp, delim)
+/*     VSTRING *buf;
+/*     ARGV    *argvp;
+/*     int     delim;
+/*
 /*     void    ARGV_FAKE_BEGIN(argv, arg)
 /*     const char *arg;
 /*
 /*     starting at the specified array position. The result is
 /*     null-terminated.
 /*
+/*     argv_join() joins all elements in an array using the
+/*     specified delimiter value, and appends the result to the
+/*     specified buffer.
+/*
 /*     ARGV_FAKE_BEGIN/END are an optimization for the case where
 /*     a single string needs to be passed into an ARGV-based
 /*     interface.  ARGV_FAKE_BEGIN() opens a statement block and
 
 #include "mymalloc.h"
 #include "msg.h"
+#include "vstring.h"
 #include "argv.h"
 
 #ifdef TEST
@@ -379,6 +389,20 @@ void    argv_delete(ARGV *argvp, ssize_t first, ssize_t how_many)
     argvp->argc -= how_many;
 }
 
+/* argv_join - concatenate array elements with delimiter */
+
+char   *argv_join(VSTRING *buf, ARGV *argv, int delim)
+{
+    char  **cpp;
+
+    for (cpp = argv->argv; *cpp; cpp++) {
+       vstring_strcat(buf, *cpp);
+       if (cpp[1])
+           VSTRING_ADDCH(buf, delim);
+    }
+    return (vstring_str(buf));
+}
+
 #ifdef TEST
 
  /*
@@ -402,6 +426,7 @@ typedef struct TEST_CASE {
     const char *exp_panic_msg;         /* expected panic */
     int     exp_argc;                  /* expected array length */
     const char *exp_argv[ARRAY_LEN];   /* expected array content */
+    int     join_delim;                        /* argv_join() delimiter */
 } TEST_CASE;
 
 #define TERMINATE_ARRAY        (1)
@@ -559,6 +584,24 @@ static ARGV *test_argv_bad_delete3(const TEST_CASE *tp, ARGV *argvp)
     return (argvp);
 }
 
+/* test_argv_join - populate, join, and overwrite */
+
+static ARGV *test_argv_join(const TEST_CASE *tp, ARGV *argvp)
+{
+    VSTRING *buf = vstring_alloc(100);
+
+    /*
+     * Impedance mismatch: argv_join() produces output to VSTRING, but the
+     * test fixture wants output to ARGV.
+     */
+    test_argv_populate(tp, argvp);
+    argv_join(buf, argvp, tp->join_delim);
+    argv_delete(argvp, 0, argvp->argc);
+    argv_add(argvp, vstring_str(buf), ARGV_END);
+    vstring_free(buf);
+    return (argvp);
+}
+
 /* test_argv_verify - verify result */
 
 static int test_argv_verify(const TEST_CASE *tp, ARGV *argvp)
@@ -573,7 +616,7 @@ static int test_argv_verify(const TEST_CASE *tp, ARGV *argvp)
        }
        if (strcmp(vstring_str(test_panic_str), tp->exp_panic_msg) != 0) {
            msg_warn("test case '%s': got '%s', want: '%s'",
-                    tp->label, vstring_str(test_panic_str), tp->exp_panic_msg);
+                tp->label, vstring_str(test_panic_str), tp->exp_panic_msg);
            return (FAIL);
        }
        return (PASS);
@@ -682,6 +725,18 @@ static const TEST_CASE test_cases[] = {
        {"foo", "baz", "bar", 0}, 0, test_argv_bad_delete3,
        "argv_delete bad range: (start=100 count=1)"
     },
+    {"argv_join, multiple strings",
+       {"foo", "baz", "bar", 0}, 0, test_argv_join,
+       0, 1, {"foo:baz:bar", 0}, ':'
+    },
+    {"argv_join, one string",
+       {"foo", 0}, 0, test_argv_join,
+       0, 1, {"foo", 0}, ':'
+    },
+    {"argv_join, empty",
+       {0}, 0, test_argv_join,
+       0, 1, {"", 0}, ':'
+    },
     0,
 };
 
index b0098ce1303ab1f5ea0d55232c2f5bb48c6ec528..f1e746ad89cda6b5a1fbdb715ddc2ea617e706f1 100644 (file)
@@ -33,6 +33,8 @@ extern void argv_truncate(ARGV *, ssize_t);
 extern void argv_insert_one(ARGV *, ssize_t, const char *);
 extern void argv_replace_one(ARGV *, ssize_t, const char *);
 extern void argv_delete(ARGV *, ssize_t, ssize_t);
+struct VSTRING;
+extern char *argv_join(struct VSTRING *buf, ARGV *, int);
 extern ARGV *argv_free(ARGV *);
 
 extern ARGV *argv_split(const char *, const char *);