]> git.ipfire.org Git - thirdparty/postfix.git/commitdiff
postfix-3.12-20260809 master
authorWietse Z Venema <wietse@porcupine.org>
Sun, 9 Aug 2026 05:00:00 +0000 (00:00 -0500)
committerViktor Dukhovni <ietf-dane@dukhovni.org>
Sun, 9 Aug 2026 13:49:07 +0000 (23:49 +1000)
postfix/HISTORY
postfix/src/global/mail_version.h
postfix/src/postdrop/Makefile.in
postfix/src/postdrop/postdrop.c
postfix/src/virtual/Makefile.in
postfix/src/virtual/mailbox.c
postfix/src/virtual/mailbox_test.c [new file with mode: 0644]

index 8d9cfd50b6b143b4bfe2bb5c351ec9a411cb8b1f..ea84639e2a7faf7291c66603b8e553bb9f666a60 100644 (file)
@@ -31883,6 +31883,37 @@ Apologies for any names omitted.
        with Oracle MySQL 8 and later. Report and fix by OpenAI
        Security. File: dict_mysql.c.
 
        with Oracle MySQL 8 and later. Report and fix by OpenAI
        Security. File: dict_mysql.c.
 
+20250807
+
+       Hardening command-line email submission: the postdrop
+       command now disallows null and line-break characters in
+       queue file envelope records (line-break characters in other
+       queue file records are already neutralized by default with
+       "cleanup_replace_stray_cr_lf = yes").
+
+       The new constraint not only eliminates line-break injection
+       into local mailbox files as reported by OpenAI Security,
+       but also prevents other forms of misuse. Later, this
+       constraint may be moved into the Postfix core. Fix by Wietse.
+       File: postdrop.c.
+
+20260806
+
+       Shut up nagging from multiple AIs and harden the virtual
+       delivery agent against an evil SQL database. Files:
+       virtual/mailbox.c, virtual/mailbox_test.c.
+
+       Bug (defect introduced: Postfix 1.1, date: 20021116): address
+       verification cache poisoning. A local user could use the
+       postdrop command to submit an address verification probe
+       with envelope or message content that Postfix will reject
+       later, resulting in a negative address verification cache
+       entry for that address. On systems that enable address
+       verification, the negative address verification cache entry
+       would force the Postfix SMTP server to reject a message
+       that it should accept (denial of service). Problem reported
+       by OpenAI Security. File: postdrop.c.
+
 TODO
 
        Reorganize PTEST_LIB, PMOCK_LIB, TESTLIB, TESTLIBS, etc.
 TODO
 
        Reorganize PTEST_LIB, PMOCK_LIB, TESTLIB, TESTLIBS, etc.
index 585b82df62b993123ce24ef40e60a2a44bb58ef6..9b5e08b321f1ee032bee787db06a6de779227d52 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.
   */
   * Patches change both the patchlevel and the release date. Snapshots have no
   * patchlevel; they change the release date only.
   */
-#define MAIL_RELEASE_DATE      "20260805"
+#define MAIL_RELEASE_DATE      "20260809"
 #define MAIL_VERSION_NUMBER    "3.12"
 
 #ifdef SNAPSHOT
 #define MAIL_VERSION_NUMBER    "3.12"
 
 #ifdef SNAPSHOT
index a6d44fedd351b369cb3300cf02bd14dc2cf5765b..804a7fd5cb168c0163064ede015cdb6b0c449b23 100644 (file)
@@ -52,7 +52,9 @@ postdrop.o: ../../include/attr.h
 postdrop.o: ../../include/check_arg.h
 postdrop.o: ../../include/clean_env.h
 postdrop.o: ../../include/cleanup_user.h
 postdrop.o: ../../include/check_arg.h
 postdrop.o: ../../include/clean_env.h
 postdrop.o: ../../include/cleanup_user.h
+postdrop.o: ../../include/deliver_request.h
 postdrop.o: ../../include/dict.h
 postdrop.o: ../../include/dict.h
+postdrop.o: ../../include/dsn.h
 postdrop.o: ../../include/htable.h
 postdrop.o: ../../include/iostuff.h
 postdrop.o: ../../include/login_sender_match.h
 postdrop.o: ../../include/htable.h
 postdrop.o: ../../include/iostuff.h
 postdrop.o: ../../include/login_sender_match.h
@@ -67,6 +69,7 @@ postdrop.o: ../../include/mail_task.h
 postdrop.o: ../../include/mail_version.h
 postdrop.o: ../../include/maillog_client.h
 postdrop.o: ../../include/msg.h
 postdrop.o: ../../include/mail_version.h
 postdrop.o: ../../include/maillog_client.h
 postdrop.o: ../../include/msg.h
+postdrop.o: ../../include/msg_stats.h
 postdrop.o: ../../include/msg_vstream.h
 postdrop.o: ../../include/myflock.h
 postdrop.o: ../../include/mymalloc.h
 postdrop.o: ../../include/msg_vstream.h
 postdrop.o: ../../include/myflock.h
 postdrop.o: ../../include/mymalloc.h
@@ -74,6 +77,7 @@ postdrop.o: ../../include/mypwd.h
 postdrop.o: ../../include/nvtable.h
 postdrop.o: ../../include/rec_attr_map.h
 postdrop.o: ../../include/rec_type.h
 postdrop.o: ../../include/nvtable.h
 postdrop.o: ../../include/rec_attr_map.h
 postdrop.o: ../../include/rec_type.h
+postdrop.o: ../../include/recipient_list.h
 postdrop.o: ../../include/record.h
 postdrop.o: ../../include/stringops.h
 postdrop.o: ../../include/sys_defs.h
 postdrop.o: ../../include/record.h
 postdrop.o: ../../include/stringops.h
 postdrop.o: ../../include/sys_defs.h
index 1ce06ed40ea3bb3b1d8fa020257abe6014415211..df48f8685c04203a5b4e1941b4f08a7b0c8a063a 100644 (file)
 
 /* Global library. */
 
 
 /* Global library. */
 
+#include <deliver_request.h>
 #include <mail_proto.h>
 #include <mail_queue.h>
 #include <mail_params.h>
 #include <mail_proto.h>
 #include <mail_queue.h>
 #include <mail_params.h>
@@ -514,6 +515,11 @@ int     main(int argc, char **argv)
            msg_fatal("uid=%ld: malformed input", (long) uid);
        if (rec_type == 0 || strchr(*expected, rec_type) == 0)
            msg_fatal("uid=%ld: unexpected record type: %d", (long) uid, rec_type);
            msg_fatal("uid=%ld: malformed input", (long) uid);
        if (rec_type == 0 || strchr(*expected, rec_type) == 0)
            msg_fatal("uid=%ld: unexpected record type: %d", (long) uid, rec_type);
+       /* 2092607 OpenAI: reject line breaks and nulls in envelope content. */
+       if (rec_type != REC_TYPE_NORM && rec_type != REC_TYPE_CONT
+           && strcspn(vstring_str(buf), "\r\n") != VSTRING_LEN(buf))
+           msg_fatal("uid=%ld: null or line break in '%s' record type: %.200s",
+                     (long) uid, rec_type_name(rec_type), vstring_str(buf));
        if (rec_type == **expected)
            expected++;
        /* Override time information from the untrusted caller. */
        if (rec_type == **expected)
            expected++;
        /* Override time information from the untrusted caller. */
@@ -536,6 +542,19 @@ int     main(int argc, char **argv)
            }
 #define STREQ(x,y) (strcmp(x,y) == 0)
 
            }
 #define STREQ(x,y) (strcmp(x,y) == 0)
 
+           /* 202607 OpenAI: allow only sendmail '-v' and '-bv' tracing. */
+           if (STREQ(attr_name, MAIL_ATTR_TRACE_FLAGS)) {
+               int     tflags = atoi(attr_value);
+
+               if (tflags == DEL_REQ_FLAG_USR_VRFY
+                   || tflags == DEL_REQ_FLAG_RECORD)
+                   rec_fprintf(dst->stream, REC_TYPE_ATTR, "%s=%d",
+                               attr_name, tflags);
+               else
+                   msg_warn("uid=%ld: ignoring unexpected trace flags: %.200s",
+                            (long) uid, attr_value);
+               continue;
+           }
            if ((STREQ(attr_name, MAIL_ATTR_ENCODING)
                 && (STREQ(attr_value, MAIL_ATTR_ENC_7BIT)
                     || STREQ(attr_value, MAIL_ATTR_ENC_8BIT)
            if ((STREQ(attr_name, MAIL_ATTR_ENCODING)
                 && (STREQ(attr_value, MAIL_ATTR_ENC_7BIT)
                     || STREQ(attr_value, MAIL_ATTR_ENC_8BIT)
@@ -545,8 +564,7 @@ int     main(int argc, char **argv)
                || rec_attr_map(attr_name)
                || (STREQ(attr_name, MAIL_ATTR_RWR_CONTEXT)
                    && (STREQ(attr_value, MAIL_ATTR_RWR_LOCAL)
                || rec_attr_map(attr_name)
                || (STREQ(attr_name, MAIL_ATTR_RWR_CONTEXT)
                    && (STREQ(attr_value, MAIL_ATTR_RWR_LOCAL)
-                       || STREQ(attr_value, MAIL_ATTR_RWR_REMOTE)))
-               || STREQ(attr_name, MAIL_ATTR_TRACE_FLAGS)) {   /* XXX */
+                       || STREQ(attr_value, MAIL_ATTR_RWR_REMOTE)))) {
                rec_fprintf(dst->stream, REC_TYPE_ATTR, "%s=%s",
                            attr_name, attr_value);
            } else {
                rec_fprintf(dst->stream, REC_TYPE_ATTR, "%s=%s",
                            attr_name, attr_value);
            } else {
index 2526526f426c9c72daa752c00f591933ae60b980..0958f76ed35c4ce5b28b833eaff03f9aec6af9d6 100644 (file)
@@ -2,11 +2,12 @@ SHELL = /bin/sh
 SRCS   = virtual.c mailbox.c recipient.c deliver_attr.c maildir.c unknown.c
 OBJS   = virtual.o mailbox.o recipient.o deliver_attr.o maildir.o unknown.o
 HDRS   = virtual.h
 SRCS   = virtual.c mailbox.c recipient.c deliver_attr.c maildir.c unknown.c
 OBJS   = virtual.o mailbox.o recipient.o deliver_attr.o maildir.o unknown.o
 HDRS   = virtual.h
-TESTSRC        =
+TESTSRC        = mailbox_test.c
 DEFS   = -I. -I$(INC_DIR) -D$(SYSTYPE)
 CFLAGS = $(DEBUG) $(OPT) $(DEFS)
 PROG   = virtual
 DEFS   = -I. -I$(INC_DIR) -D$(SYSTYPE)
 CFLAGS = $(DEBUG) $(OPT) $(DEFS)
 PROG   = virtual
-TESTPROG= 
+TESTPROG= mailbox_test
+TESTLIB= ../../lib/libptest.a
 INC_DIR        = ../../include
 LIBS   = ../../lib/lib$(LIB_PREFIX)master$(LIB_SUFFIX) \
        ../../lib/lib$(LIB_PREFIX)global$(LIB_SUFFIX) \
 INC_DIR        = ../../include
 LIBS   = ../../lib/lib$(LIB_PREFIX)master$(LIB_SUFFIX) \
        ../../lib/lib$(LIB_PREFIX)global$(LIB_SUFFIX) \
@@ -24,7 +25,7 @@ Makefile: Makefile.in
 
 test:  $(TESTPROG)
 
 
 test:  $(TESTPROG)
 
-tests:
+tests: test_mailbox
 
 root_tests:
 
 
 root_tests:
 
@@ -38,6 +39,14 @@ clean:
 
 tidy:  clean
 
 
 tidy:  clean
 
+MAILBOX_TEST_OBJ = mailbox.o $(TESTLIB)
+
+mailbox_test: mailbox_test.o $(MAILBOX_TEST_OBJ) $(LIBS)
+       $(CC) $(CFLAGS) -o $@ $@.o $(MAILBOX_TEST_OBJ) $(LIBS) $(SYSLIBS)
+
+test_mailbox: mailbox_test
+       $(SHLIB_ENV) $(VALGRIND) ./mailbox_test
+
 depend: $(MAKES)
        (sed '1,/^# do not edit/!d' Makefile.in; \
        set -e; for i in [a-z][a-z0-9]*.c; do \
 depend: $(MAKES)
        (sed '1,/^# do not edit/!d' Makefile.in; \
        set -e; for i in [a-z][a-z0-9]*.c; do \
@@ -96,6 +105,7 @@ mailbox.o: ../../include/nvtable.h
 mailbox.o: ../../include/pol_stats.h
 mailbox.o: ../../include/recipient_list.h
 mailbox.o: ../../include/safe_open.h
 mailbox.o: ../../include/pol_stats.h
 mailbox.o: ../../include/recipient_list.h
 mailbox.o: ../../include/safe_open.h
+mailbox.o: ../../include/sane_strtol.h
 mailbox.o: ../../include/sent.h
 mailbox.o: ../../include/set_eugid.h
 mailbox.o: ../../include/stringops.h
 mailbox.o: ../../include/sent.h
 mailbox.o: ../../include/set_eugid.h
 mailbox.o: ../../include/stringops.h
@@ -105,6 +115,40 @@ mailbox.o: ../../include/vstream.h
 mailbox.o: ../../include/vstring.h
 mailbox.o: mailbox.c
 mailbox.o: virtual.h
 mailbox.o: ../../include/vstring.h
 mailbox.o: mailbox.c
 mailbox.o: virtual.h
+mailbox_test.o: ../../include/argv.h
+mailbox_test.o: ../../include/attr.h
+mailbox_test.o: ../../include/bounce.h
+mailbox_test.o: ../../include/check_arg.h
+mailbox_test.o: ../../include/defer.h
+mailbox_test.o: ../../include/deliver_request.h
+mailbox_test.o: ../../include/dict.h
+mailbox_test.o: ../../include/dsn.h
+mailbox_test.o: ../../include/dsn_buf.h
+mailbox_test.o: ../../include/htable.h
+mailbox_test.o: ../../include/mail_params.h
+mailbox_test.o: ../../include/maps.h
+mailbox_test.o: ../../include/mbox_conf.h
+mailbox_test.o: ../../include/msg.h
+mailbox_test.o: ../../include/msg_jmp.h
+mailbox_test.o: ../../include/msg_output.h
+mailbox_test.o: ../../include/msg_stats.h
+mailbox_test.o: ../../include/msg_vstream.h
+mailbox_test.o: ../../include/myflock.h
+mailbox_test.o: ../../include/mymalloc.h
+mailbox_test.o: ../../include/myrand.h
+mailbox_test.o: ../../include/nvtable.h
+mailbox_test.o: ../../include/pmock_expect.h
+mailbox_test.o: ../../include/pol_stats.h
+mailbox_test.o: ../../include/ptest.h
+mailbox_test.o: ../../include/ptest_main.h
+mailbox_test.o: ../../include/recipient_list.h
+mailbox_test.o: ../../include/stringops.h
+mailbox_test.o: ../../include/sys_defs.h
+mailbox_test.o: ../../include/vbuf.h
+mailbox_test.o: ../../include/vstream.h
+mailbox_test.o: ../../include/vstring.h
+mailbox_test.o: mailbox_test.c
+mailbox_test.o: virtual.h
 maildir.o: ../../include/argv.h
 maildir.o: ../../include/attr.h
 maildir.o: ../../include/bounce.h
 maildir.o: ../../include/argv.h
 maildir.o: ../../include/attr.h
 maildir.o: ../../include/bounce.h
index 19afca877c146eaa9559e21a583fe55f6f42bdec..39d08fa5eb07a549186cf20e1f20d0bf3a910660 100644 (file)
@@ -57,6 +57,7 @@
 #include <mymalloc.h>
 #include <stringops.h>
 #include <set_eugid.h>
 #include <mymalloc.h>
 #include <stringops.h>
 #include <set_eugid.h>
+#include <sane_strtol.h>
 
 /* Global library. */
 
 
 /* Global library. */
 
@@ -176,7 +177,8 @@ int     deliver_mailbox(LOCAL_STATE state, USER_ATTR usr_attr, int *statusp)
     const char *uid_res;
     const char *gid_res;
     DSN_BUF *why = state.msg_attr.why;
     const char *uid_res;
     const char *gid_res;
     DSN_BUF *why = state.msg_attr.why;
-    long    n;
+    char   *end;
+    unsigned long n;
 
     /*
      * Make verbose logging easier to understand.
 
     /*
      * Make verbose logging easier to understand.
@@ -215,6 +217,15 @@ int     deliver_mailbox(LOCAL_STATE state, USER_ATTR usr_attr, int *statusp)
 
 #define RETURN(res) { myfree(usr_attr.mailbox); return (res); }
 
 
 #define RETURN(res) { myfree(usr_attr.mailbox); return (res); }
 
+    if (strstr(usr_attr.mailbox + strlen(var_virt_mailbox_base), "/../")) {
+       msg_warn("recipient %s: bad mailbox path %s in %s",
+            state.msg_attr.user, mailbox_res, virtual_mailbox_maps->title);
+       dsb_simple(why, "4.3.5", "mail system configuration error");
+       *statusp = defer_append(BOUNCE_FLAGS(state.request),
+                               BOUNCE_ATTR(state.msg_attr));
+       RETURN(YES);
+    }
+
     /*
      * Look up the mailbox owner rights. Defer in case of trouble.
      */
     /*
      * Look up the mailbox owner rights. Defer in case of trouble.
      */
@@ -228,7 +239,10 @@ int     deliver_mailbox(LOCAL_STATE state, USER_ATTR usr_attr, int *statusp)
                                BOUNCE_ATTR(state.msg_attr));
        RETURN(YES);
     }
                                BOUNCE_ATTR(state.msg_attr));
        RETURN(YES);
     }
-    if ((n = atol(uid_res)) < var_virt_minimum_uid) {
+    usr_attr.uid = (uid_t) (n = sane_strtoul(uid_res, &end, 10));
+    if (*end != 0 || errno != 0 || usr_attr.uid != n
+       || usr_attr.uid == (uid_t) - 1          /* Special for safe_open()) */
+       || usr_attr.uid < var_virt_minimum_uid) {
        msg_warn("recipient %s: bad uid %s in %s",
                 state.msg_attr.user, uid_res, virtual_uid_maps->title);
        dsb_simple(why, "4.3.5", "mail system configuration error");
        msg_warn("recipient %s: bad uid %s in %s",
                 state.msg_attr.user, uid_res, virtual_uid_maps->title);
        dsb_simple(why, "4.3.5", "mail system configuration error");
@@ -236,7 +250,6 @@ int     deliver_mailbox(LOCAL_STATE state, USER_ATTR usr_attr, int *statusp)
                                BOUNCE_ATTR(state.msg_attr));
        RETURN(YES);
     }
                                BOUNCE_ATTR(state.msg_attr));
        RETURN(YES);
     }
-    usr_attr.uid = (uid_t) n;
 
     /*
      * Look up the mailbox group rights. Defer in case of trouble.
 
     /*
      * Look up the mailbox group rights. Defer in case of trouble.
@@ -251,7 +264,9 @@ int     deliver_mailbox(LOCAL_STATE state, USER_ATTR usr_attr, int *statusp)
                                BOUNCE_ATTR(state.msg_attr));
        RETURN(YES);
     }
                                BOUNCE_ATTR(state.msg_attr));
        RETURN(YES);
     }
-    if ((n = atol(gid_res)) <= 0) {
+    usr_attr.gid = (gid_t) (n = sane_strtoul(gid_res, &end, 10));
+    if (*end != 0 || errno != 0 || usr_attr.gid !=n
+       || usr_attr.gid == (gid_t) - 1) {       /* Special for safe_open()) */
        msg_warn("recipient %s: bad gid %s in %s",
                 state.msg_attr.user, gid_res, virtual_gid_maps->title);
        dsb_simple(why, "4.3.5", "mail system configuration error");
        msg_warn("recipient %s: bad gid %s in %s",
                 state.msg_attr.user, gid_res, virtual_gid_maps->title);
        dsb_simple(why, "4.3.5", "mail system configuration error");
@@ -259,12 +274,10 @@ int     deliver_mailbox(LOCAL_STATE state, USER_ATTR usr_attr, int *statusp)
                                BOUNCE_ATTR(state.msg_attr));
        RETURN(YES);
     }
                                BOUNCE_ATTR(state.msg_attr));
        RETURN(YES);
     }
-    usr_attr.gid = (gid_t) n;
-
     if (msg_verbose)
     if (msg_verbose)
-       msg_info("%s[%d]: set user_attr: %s, uid = %u, gid = %u",
+       msg_info("%s[%d]: set user_attr: %s, uid = %lu, gid = %lu",
                 myname, state.level, usr_attr.mailbox,
                 myname, state.level, usr_attr.mailbox,
-                (unsigned) usr_attr.uid, (unsigned) usr_attr.gid);
+                (unsigned long) usr_attr.uid, (unsigned long) usr_attr.gid);
 
     /*
      * Deliver to mailbox or to maildir.
 
     /*
      * Deliver to mailbox or to maildir.
diff --git a/postfix/src/virtual/mailbox_test.c b/postfix/src/virtual/mailbox_test.c
new file mode 100644 (file)
index 0000000..5b7ff73
--- /dev/null
@@ -0,0 +1,366 @@
+ /*
+  * System library.
+  */
+#include <sys_defs.h>
+#include <string.h>
+
+ /*
+  * Global library.
+  */
+#include <bounce.h>
+#include <defer.h>
+#include <deliver_request.h>
+#include <mail_params.h>
+#include <maps.h>
+
+ /*
+  * Application-specific.
+  */
+#include <virtual.h>
+
+ /*
+  * Test library.
+  */
+#include <ptest.h>
+
+ /*
+  * Test case and data.
+  */
+typedef struct PTEST_CASE {
+    char *testname;
+    void    (*action) (PTEST_CTX *t, const struct PTEST_CASE *);
+    char *user;
+    int     minimum_uid;
+    char *mailbox_maps;
+    char *uid_maps;
+    char *gid_maps;
+    char *mailbox_base;
+    char *mailbox_lock;
+    char **want_log;
+    int     want_status;
+    int     want_known;
+} PTEST_CASE;
+
+#define STATUS_FINAL   0
+#define STATUS_DEFER   1
+#define STATUS_UNSET   -1
+
+#define USER_UNKNOWN   0
+#define USER_KNOWN     1
+ /*
+  * Surriogate parameter dependencies.
+  */
+int     var_virt_minimum_uid;
+char   *var_virt_mailbox_maps;
+char   *var_virt_uid_maps;
+char   *var_virt_gid_maps;
+char   *var_virt_mailbox_base;
+char   *var_virt_mailbox_lock;
+bool    var_strict_mbox_owner;
+int     virtual_mbox_lock_mask;
+char   *var_rcpt_delim = "+";
+
+ /*
+  * Data dependencies.
+  */
+MAPS   *virtual_mailbox_maps;
+MAPS   *virtual_uid_maps;
+MAPS   *virtual_gid_maps;
+
+static LOCAL_STATE state;
+static USER_ATTR usr_attr;
+static DELIVER_REQUEST request;
+
+ /*
+  * Surrogate code dependencies.
+  * 
+  * deliver_mailbox() will call deliver_mailbox_file() in the same file which
+  * has too many dependencies. Instead we trigger maildir-style delivery and
+  * use a fake deliver_maildir() to verify some of the arguments.
+  * 
+  * defer_append() has too many dependencies. Instead we use a fake
+  * defer_append() to verify some of the arguments.
+  */
+int     deliver_maildir(LOCAL_STATE state, USER_ATTR user_attr)
+{
+    msg_info("fake deliver_maildir: mailbox='%s', uid=%lu, gid=%lu",
+            user_attr.mailbox, (unsigned long) user_attr.uid,
+            (unsigned long) user_attr.gid);
+    return (0);
+}
+
+int     defer_append(int flags, const char *id, MSG_STATS *stats,
+                            RECIPIENT *rcpt, const char *relay,
+                            const POL_STATS *tstats, DSN *dsn)
+{
+    msg_info("fake defer_append: dsn=%s reason=%s", dsn->status, dsn->reason);
+    return (1);
+}
+
+static void teardown_test(void)
+{
+    if (virtual_mailbox_maps) {
+       maps_free(virtual_mailbox_maps);
+       virtual_mailbox_maps = 0;
+    }
+    if (virtual_uid_maps) {
+       maps_free(virtual_uid_maps);
+       virtual_uid_maps = 0;
+    }
+    if (virtual_gid_maps) {
+       maps_free(virtual_gid_maps);
+       virtual_gid_maps = 0;
+    }
+    if (state.msg_attr.why) {
+       dsb_free(state.msg_attr.why);
+       state.msg_attr.why = 0;
+    }
+}
+
+static void setup_test(const PTEST_CASE *tp)
+{
+    /* In case a previous test failed. */
+    teardown_test();
+
+    /*
+     * Set parameters so that warning messages log as expected.
+     */
+    var_virt_minimum_uid = tp->minimum_uid;
+    var_virt_mailbox_maps = tp->mailbox_maps;
+    var_virt_uid_maps = tp->uid_maps;
+    var_virt_gid_maps = tp->gid_maps;
+    var_virt_mailbox_base = tp->mailbox_base;
+    var_virt_mailbox_lock = tp->mailbox_lock;
+
+    /*
+     * Open databases.
+     */
+    if (var_virt_mailbox_maps)
+       virtual_mailbox_maps =
+           maps_create(VAR_VIRT_MAILBOX_MAPS, var_virt_mailbox_maps,
+                       DICT_FLAG_LOCK | DICT_FLAG_PARANOID
+                       | DICT_FLAG_UTF8_REQUEST);
+    if (var_virt_uid_maps)
+       virtual_uid_maps =
+           maps_create(VAR_VIRT_UID_MAPS, var_virt_uid_maps,
+                       DICT_FLAG_LOCK | DICT_FLAG_PARANOID
+                       | DICT_FLAG_UTF8_REQUEST);
+    if (var_virt_gid_maps)
+       virtual_gid_maps =
+           maps_create(VAR_VIRT_GID_MAPS, var_virt_gid_maps,
+                       DICT_FLAG_LOCK | DICT_FLAG_PARANOID
+                       | DICT_FLAG_UTF8_REQUEST);
+
+    /*
+     * Initialize local state, request, and user attributes.
+     */
+    state.msg_attr.why = dsb_create();;
+    state.msg_attr.user = tp->user;
+    state.msg_attr.rcpt.address = tp->user;
+    state.msg_attr.delivered = tp->user;
+    request.flags = 0;
+    state.request = &request;
+}
+
+static void test_deliver_mailbox(PTEST_CTX *t, const struct PTEST_CASE *tp)
+{
+    int     got_status = STATUS_UNSET;
+    int     got_known;
+    char  **cpp;
+
+    if (sizeof(uid_t) > 4) {
+       ptest_info(t, "uid_t is too large -- skipping this test");
+       ptest_skip(t);
+    }
+    if (sizeof(long) <= 4) {
+       ptest_info(t, "long is too small -- skipping this test");
+       ptest_skip(t);
+    }
+    setup_test(tp);
+    if (tp->want_log)
+       for (cpp = (char **) tp->want_log; *cpp; cpp++)
+           expect_ptest_log_event(t, *cpp);
+    got_known = deliver_mailbox(state, usr_attr, &got_status);
+    if (got_known != tp->want_known)
+       ptest_error(t, "user known: got %d, want %d",
+                   got_known, tp->want_known);
+    if (got_status != tp->want_status)
+       ptest_error(t, "user status: got %d, want %d",
+                   got_status, tp->want_status);
+    teardown_test();
+}
+
+ /*
+  * Test cases.
+  */
+const PTEST_CASE ptestcases[] = {
+    {
+       .testname = "normal case",
+       .action = test_deliver_mailbox,
+       .user = "example-user",
+       .minimum_uid = 1,
+       .mailbox_maps = "static:user-1-1/",
+       .uid_maps = "static:1",
+       .gid_maps = "static:1",
+       .mailbox_base = "/base",
+       .want_log = (char *[]) {"mailbox='/base/user-1-1/', uid=1, gid=1", 0,},
+       .want_known = USER_KNOWN,
+       .want_status = USER_UNKNOWN,
+    },{
+       .testname = "relative base",
+       .action = test_deliver_mailbox,
+       .user = "example-user",
+       .mailbox_base = "base",
+       .want_log = (char *[]) {"do not specify relative pathname", 0,},
+       .want_known = USER_UNKNOWN,
+       .want_status = STATUS_UNSET,
+    },{
+       .testname = "user unknown",
+       .action = test_deliver_mailbox,
+       .user = "example-user",
+       .mailbox_maps = "inline:{x=x}",
+       .mailbox_base = "/base",
+       .want_known = USER_UNKNOWN,
+       .want_status = STATUS_UNSET,
+    },{
+       .testname = "trailing .. in path",
+       .action = test_deliver_mailbox,
+       .user = "example-user",
+       .mailbox_maps = "static:here/../",
+       .mailbox_base = "/base",
+       .want_log = (char *[]) {
+           "recipient example-user: bad mailbox path here/../",
+           "dsn=4.3.5 reason=mail system configuration error",
+           0,
+       },
+       .want_known = USER_KNOWN,
+       .want_status = STATUS_DEFER,
+    },{
+       .testname = "leading .. in path",
+       .action = test_deliver_mailbox,
+       .user = "example-user",
+       .mailbox_maps = "static:../here/",
+       .mailbox_base = "/base",
+       .want_log = (char *[]) {
+           "recipient example-user: bad mailbox path ../here/",
+           "dsn=4.3.5 reason=mail system configuration error",
+           0,
+       },
+       .want_known = USER_KNOWN,
+       .want_status = STATUS_DEFER,
+    },{
+       .testname = "missing entry in virtual_gid_maps",
+       .action = test_deliver_mailbox,
+       .user = "example-user",
+       .minimum_uid = 1,
+       .mailbox_maps = "static:user-1-1/",
+       .uid_maps = "static:1",
+       .gid_maps = "inline:{x=x}",
+       .mailbox_base = "/base",
+       .want_log = (char *[]) {
+           "recipient example-user: not found in virtual_gid_maps", 
+           "dsn=4.3.5 reason=mail system configuration error",
+           0,
+       },
+       .want_known = USER_KNOWN,
+       .want_status = STATUS_DEFER,
+    },{
+       .testname = "missing entry in virtual_uid_maps",
+       .action = test_deliver_mailbox,
+       .user = "example-user",
+       .minimum_uid = 1,
+       .mailbox_maps = "static:user-1-1/",
+       .uid_maps = "inline:{x=x}",
+       .mailbox_base = "/base",
+       .want_log = (char *[]) {
+           "recipient example-user: not found in virtual_uid_maps", 
+           "dsn=4.3.5 reason=mail system configuration error",
+           0,
+       },
+       .want_known = USER_KNOWN,
+       .want_status = STATUS_DEFER,
+    },{
+       .testname = "gid too large",
+       .action = test_deliver_mailbox,
+       .user = "example-user",
+       .minimum_uid = 1,
+       .mailbox_maps = "static:user-1-1/",
+       .uid_maps = "static:1",
+       .gid_maps = "static:9223372036854775807L",
+       .mailbox_base = "/base",
+       .want_log = (char *[]) {
+           "bad gid 9223372036854775807L in virtual_gid_maps", 
+           "dsn=4.3.5 reason=mail system configuration error",
+           0,
+       },
+       .want_known = USER_KNOWN,
+       .want_status = STATUS_DEFER,
+    },{
+       .testname = "uid too large",
+       .action = test_deliver_mailbox,
+       .user = "example-user",
+       .minimum_uid = 1,
+       .mailbox_maps = "static:user-1-1/",
+       .uid_maps = "static:9223372036854775807L",
+       .gid_maps = "static:1",
+       .mailbox_base = "/base",
+       .want_log = (char *[]) {
+           "bad uid 9223372036854775807L in virtual_uid_maps", 
+           "dsn=4.3.5 reason=mail system configuration error",
+           0,
+       },
+       .want_known = USER_KNOWN,
+       .want_status = STATUS_DEFER,
+    },{
+       .testname = "bad uid conversion",
+       .action = test_deliver_mailbox,
+       .user = "example-user",
+       .minimum_uid = 1,
+       .mailbox_maps = "static:user-1-1/",
+       .uid_maps = "static:9223372X36854775807L",
+       .gid_maps = "static:1",
+       .mailbox_base = "/base",
+       .want_log = (char *[]) {
+           "bad uid 9223372X36854775807L in virtual_uid_maps", 
+           "dsn=4.3.5 reason=mail system configuration error",
+           0,
+       },
+       .want_known = USER_KNOWN,
+       .want_status = STATUS_DEFER,
+    },{
+       .testname = "bad gid conversion",
+       .action = test_deliver_mailbox,
+       .user = "example-user",
+       .minimum_uid = 1,
+       .mailbox_maps = "static:user-1-1/",
+       .uid_maps = "static:1",
+       .gid_maps = "static:9223372X36854775807L",
+       .mailbox_base = "/base",
+       .want_log = (char *[]) {
+           "bad gid 9223372X36854775807L in virtual_gid_maps", 
+           "dsn=4.3.5 reason=mail system configuration error",
+           0,
+       },
+       .want_known = USER_KNOWN,
+       .want_status = STATUS_DEFER,
+    }, {
+       .testname = "uid too small",
+       .action = test_deliver_mailbox,
+       .user = "example-user",
+       .minimum_uid = 1,
+       .mailbox_maps = "static:user-1-1/",
+       .uid_maps = "static:0",
+       .gid_maps = "static:1",
+       .mailbox_base = "/base",
+       .want_log = (char *[]) {
+           "bad uid 0 in virtual_uid_maps", 
+           "dsn=4.3.5 reason=mail system configuration error",
+           0,
+       },
+       .want_known = USER_KNOWN,
+       .want_status = STATUS_DEFER,
+    },
+    /* TODO(wietse) database error returns. */
+};
+
+#include <ptest_main.h>