summaryrefslogtreecommitdiffstats
path: root/src/transports
diff options
context:
space:
mode:
Diffstat (limited to 'src/transports')
-rw-r--r--src/transports/Makefile27
-rw-r--r--src/transports/README41
-rw-r--r--src/transports/appendfile.c3317
-rw-r--r--src/transports/appendfile.h100
-rw-r--r--src/transports/autoreply.c821
-rw-r--r--src/transports/autoreply.h45
-rw-r--r--src/transports/lmtp.c809
-rw-r--r--src/transports/lmtp.h32
-rw-r--r--src/transports/pipe.c1124
-rw-r--r--src/transports/pipe.h51
-rw-r--r--src/transports/queuefile.c286
-rw-r--r--src/transports/queuefile.h29
-rw-r--r--src/transports/smtp.c6071
-rw-r--r--src/transports/smtp.h252
-rw-r--r--src/transports/smtp_socks.c415
-rw-r--r--src/transports/tf_maildir.c585
-rw-r--r--src/transports/tf_maildir.h21
17 files changed, 14026 insertions, 0 deletions
diff --git a/src/transports/Makefile b/src/transports/Makefile
new file mode 100644
index 0000000..4eea141
--- /dev/null
+++ b/src/transports/Makefile
@@ -0,0 +1,27 @@
+# Make file for building a library containing all the available transports and
+# calling it transports.a. This is called from the main make file, after cd'ing
+# to the transports subdirectory.
+
+OBJ = appendfile.o autoreply.o lmtp.o pipe.o queuefile.o smtp.o smtp_socks.o tf_maildir.o
+
+transports.a: $(OBJ)
+ @$(RM_COMMAND) -f transports.a
+ @echo "$(AR) transports.a"
+ @$(AR) transports.a $(OBJ)
+ $(RANLIB) $@
+
+.SUFFIXES: .o .c
+.c.o:; @echo "$(CC) $*.c"
+ $(FE)$(CC) -c $(CFLAGS) $(INCLUDE) $*.c
+
+appendfile.o: $(HDRS) appendfile.c appendfile.h tf_maildir.h
+autoreply.o: $(HDRS) autoreply.c autoreply.h
+lmtp.o: $(HDRS) lmtp.c lmtp.h
+pipe.o: $(HDRS) pipe.c pipe.h
+queuefile.o: $(HDRS) queuefile.c queuefile.h
+smtp.o: $(HDRS) smtp.c smtp.h
+smtp_socks.o: $(HDRS) smtp_socks.c smtp.h
+
+tf_maildir.o: $(HDRS) tf_maildir.c tf_maildir.h appendfile.h
+
+# End
diff --git a/src/transports/README b/src/transports/README
new file mode 100644
index 0000000..9ea29fb
--- /dev/null
+++ b/src/transports/README
@@ -0,0 +1,41 @@
+TRANSPORTS:
+
+A delivery attempt results in one of the following values being placed in
+addr->transport_return:
+
+ OK success
+ DEFER temporary failure
+ FAIL permanent failure
+ PANIC disaster - causes exim to bomb
+
+The field is initialized to DEFER when the address is created, in order that
+unexpected process crashes or other problems don't cause the message to be
+deleted.
+
+For non-OK values, additional information is placed in addr->errno,
+addr->more_errno, and optionally in addr->message. These are inspected only if
+the status is not OK, with one exception (see below).
+
+In addition, the addr->special_action field can be set to request a non-default
+action. The default action after FAIL is to return to sender; the default
+action after DEFER is nothing. The alternatives are:
+
+ SPECIAL_NONE (default) no special action
+ SPECIAL_FREEZE freeze the message
+ SPECIAL_WARN send warning message
+
+The SPECIAL_WARN action is the exception referred to above. It is picked up
+only after a *successful* delivery; it causes a warning message to be sent
+containing the text of warn_message to warn_to. It can be used in appendfile,
+for example, to send a warning message when the mailbox size crosses a given
+threshold.
+
+If the transport is handling a batch of several addresses, it may either put an
+individual value in each address structure, and return TRUE, or it may put a
+common value in the first address, and return FALSE.
+
+Remote transports usually return TRUE and local transports usually return
+FALSE; however, the lmtp transport may return either value, depending on what
+happens inside it.
+
+****
diff --git a/src/transports/appendfile.c b/src/transports/appendfile.c
new file mode 100644
index 0000000..93281ef
--- /dev/null
+++ b/src/transports/appendfile.c
@@ -0,0 +1,3317 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) The Exim maintainers 2020 - 2022 */
+/* Copyright (c) University of Cambridge 1995 - 2020 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+
+#include "../exim.h"
+#include "appendfile.h"
+
+#ifdef SUPPORT_MAILDIR
+#include "tf_maildir.h"
+#endif
+
+
+/* Options specific to the appendfile transport. They must be in alphabetic
+order (note that "_" comes before the lower case letters). Some of them are
+stored in the publicly visible instance block - these are flagged with the
+opt_public flag. */
+#define LOFF(field) OPT_OFF(appendfile_transport_options_block, field)
+
+optionlist appendfile_transport_options[] = {
+#ifdef SUPPORT_MAILDIR
+ { "*expand_maildir_use_size_file", opt_stringptr, LOFF(expand_maildir_use_size_file) },
+#endif
+ { "*set_use_fcntl_lock",opt_bool | opt_hidden, LOFF(set_use_fcntl) },
+ { "*set_use_flock_lock",opt_bool | opt_hidden, LOFF(set_use_flock) },
+ { "*set_use_lockfile", opt_bool | opt_hidden, LOFF(set_use_lockfile) },
+#ifdef SUPPORT_MBX
+ { "*set_use_mbx_lock", opt_bool | opt_hidden, LOFF(set_use_mbx_lock) },
+#endif
+ { "allow_fifo", opt_bool, LOFF(allow_fifo) },
+ { "allow_symlink", opt_bool, LOFF(allow_symlink) },
+ { "batch_id", opt_stringptr | opt_public, OPT_OFF(transport_instance, batch_id) },
+ { "batch_max", opt_int | opt_public, OPT_OFF(transport_instance, batch_max) },
+ { "check_group", opt_bool, LOFF(check_group) },
+ { "check_owner", opt_bool, LOFF(check_owner) },
+ { "check_string", opt_stringptr, LOFF(check_string) },
+ { "create_directory", opt_bool, LOFF(create_directory) },
+ { "create_file", opt_stringptr, LOFF(create_file_string) },
+ { "directory", opt_stringptr, LOFF(dirname) },
+ { "directory_file", opt_stringptr, LOFF(dirfilename) },
+ { "directory_mode", opt_octint, LOFF(dirmode) },
+ { "escape_string", opt_stringptr, LOFF(escape_string) },
+ { "file", opt_stringptr, LOFF(filename) },
+ { "file_format", opt_stringptr, LOFF(file_format) },
+ { "file_must_exist", opt_bool, LOFF(file_must_exist) },
+ { "lock_fcntl_timeout", opt_time, LOFF(lock_fcntl_timeout) },
+ { "lock_flock_timeout", opt_time, LOFF(lock_flock_timeout) },
+ { "lock_interval", opt_time, LOFF(lock_interval) },
+ { "lock_retries", opt_int, LOFF(lock_retries) },
+ { "lockfile_mode", opt_octint, LOFF(lockfile_mode) },
+ { "lockfile_timeout", opt_time, LOFF(lockfile_timeout) },
+ { "mailbox_filecount", opt_stringptr, LOFF(mailbox_filecount_string) },
+ { "mailbox_size", opt_stringptr, LOFF(mailbox_size_string) },
+#ifdef SUPPORT_MAILDIR
+ { "maildir_format", opt_bool, LOFF(maildir_format ) } ,
+ { "maildir_quota_directory_regex", opt_stringptr, LOFF(maildir_dir_regex) },
+ { "maildir_retries", opt_int, LOFF(maildir_retries) },
+ { "maildir_tag", opt_stringptr, LOFF(maildir_tag) },
+ { "maildir_use_size_file", opt_expand_bool, LOFF(maildir_use_size_file ) } ,
+ { "maildirfolder_create_regex", opt_stringptr, LOFF(maildirfolder_create_regex ) },
+#endif /* SUPPORT_MAILDIR */
+#ifdef SUPPORT_MAILSTORE
+ { "mailstore_format", opt_bool, LOFF(mailstore_format ) },
+ { "mailstore_prefix", opt_stringptr, LOFF(mailstore_prefix ) },
+ { "mailstore_suffix", opt_stringptr, LOFF(mailstore_suffix ) },
+#endif /* SUPPORT_MAILSTORE */
+#ifdef SUPPORT_MBX
+ { "mbx_format", opt_bool, LOFF(mbx_format ) } ,
+#endif /* SUPPORT_MBX */
+ { "message_prefix", opt_stringptr, LOFF(message_prefix) },
+ { "message_suffix", opt_stringptr, LOFF(message_suffix) },
+ { "mode", opt_octint, LOFF(mode) },
+ { "mode_fail_narrower",opt_bool, LOFF(mode_fail_narrower) },
+ { "notify_comsat", opt_bool, LOFF(notify_comsat) },
+ { "quota", opt_stringptr, LOFF(quota) },
+ { "quota_directory", opt_stringptr, LOFF(quota_directory) },
+ { "quota_filecount", opt_stringptr, LOFF(quota_filecount) },
+ { "quota_is_inclusive", opt_bool, LOFF(quota_is_inclusive) },
+ { "quota_size_regex", opt_stringptr, LOFF(quota_size_regex) },
+ { "quota_warn_message", opt_stringptr | opt_public, OPT_OFF(transport_instance, warn_message) },
+ { "quota_warn_threshold", opt_stringptr, LOFF(quota_warn_threshold) },
+ { "use_bsmtp", opt_bool, LOFF(use_bsmtp) },
+ { "use_crlf", opt_bool, LOFF(use_crlf) },
+ { "use_fcntl_lock", opt_bool_set, LOFF(use_fcntl) },
+ { "use_flock_lock", opt_bool_set, LOFF(use_flock) },
+ { "use_lockfile", opt_bool_set, LOFF(use_lockfile) },
+#ifdef SUPPORT_MBX
+ { "use_mbx_lock", opt_bool_set, LOFF(use_mbx_lock) },
+#endif /* SUPPORT_MBX */
+};
+
+/* Size of the options list. An extern variable has to be used so that its
+address can appear in the tables drtables.c. */
+
+int appendfile_transport_options_count =
+ sizeof(appendfile_transport_options)/sizeof(optionlist);
+
+
+#ifdef MACRO_PREDEF
+
+/* Dummy values */
+appendfile_transport_options_block appendfile_transport_option_defaults = {0};
+void appendfile_transport_init(transport_instance *tblock) {}
+BOOL appendfile_transport_entry(transport_instance *tblock, address_item *addr) {return FALSE;}
+
+#else /*!MACRO_PREDEF*/
+
+/* Default private options block for the appendfile transport. */
+
+appendfile_transport_options_block appendfile_transport_option_defaults = {
+ /* all non-mentioned members zero/null/false */
+ .dirfilename = US"q${base62:$tod_epoch}-$inode",
+ .create_file_string = US"anywhere",
+ .maildir_dir_regex = US"^(?:cur|new|\\..*)$",
+ .mailbox_size_value = -1,
+ .mailbox_filecount_value = -1,
+ .mode = APPENDFILE_MODE,
+ .dirmode = APPENDFILE_DIRECTORY_MODE,
+ .lockfile_mode = APPENDFILE_LOCKFILE_MODE,
+ .lockfile_timeout = 30*60,
+ .lock_retries = 10,
+ .lock_interval = 3,
+ .maildir_retries = 10,
+ .create_file = create_anywhere,
+ .check_owner = TRUE,
+ .create_directory = TRUE,
+ .notify_comsat = FALSE,
+ .use_lockfile = TRUE,
+ .use_fcntl = TRUE,
+ .mode_fail_narrower = TRUE,
+ .quota_is_inclusive = TRUE,
+};
+
+
+/* Encodings for mailbox formats, and their names. MBX format is actually
+supported only if SUPPORT_MBX is set. */
+
+enum { mbf_unix, mbf_mbx, mbf_smail, mbf_maildir, mbf_mailstore };
+
+static const char *mailbox_formats[] = {
+ "unix", "mbx", "smail", "maildir", "mailstore" };
+
+
+/* Check warn threshold only if quota size set or not a percentage threshold
+ percentage check should only be done if quota > 0 */
+
+#define THRESHOLD_CHECK (ob->quota_warn_threshold_value > 0 && \
+ (!ob->quota_warn_threshold_is_percent || ob->quota_value > 0))
+
+
+
+/*************************************************
+* Setup entry point *
+*************************************************/
+
+/* Called for each delivery in the privileged state, just before the uid/gid
+are changed and the main entry point is called. We use this function to
+expand any quota settings, so that it can access files that may not be readable
+by the user. It is also used to pick up external mailbox size information, if
+set.
+
+Arguments:
+ tblock points to the transport instance
+ addrlist addresses about to be delivered (not used)
+ dummy not used (doesn't pass back data)
+ uid the uid that will be set (not used)
+ gid the gid that will be set (not used)
+ errmsg where to put an error message
+
+Returns: OK, FAIL, or DEFER
+*/
+
+static int
+appendfile_transport_setup(transport_instance *tblock, address_item *addrlist,
+ transport_feedback *dummy, uid_t uid, gid_t gid, uschar **errmsg)
+{
+appendfile_transport_options_block *ob =
+ (appendfile_transport_options_block *)(tblock->options_block);
+uschar *q = ob->quota;
+double default_value = 0.0;
+
+if (ob->expand_maildir_use_size_file)
+ ob->maildir_use_size_file = expand_check_condition(ob->expand_maildir_use_size_file,
+ US"`maildir_use_size_file` in transport", tblock->name);
+
+/* Loop for quota, quota_filecount, quota_warn_threshold, mailbox_size,
+mailbox_filecount */
+
+for (int i = 0; i < 5; i++)
+ {
+ double d = default_value;
+ int no_check = 0;
+ uschar *which = NULL;
+
+ if (q)
+ {
+ uschar * rest, * s;
+
+ if (!(s = expand_string(q)))
+ {
+ *errmsg = string_sprintf("Expansion of \"%s\" in %s transport failed: "
+ "%s", q, tblock->name, expand_string_message);
+ return f.search_find_defer ? DEFER : FAIL;
+ }
+
+ d = Ustrtod(s, &rest);
+
+ /* Handle following characters K, M, G, %, the latter being permitted
+ for quota_warn_threshold only. A threshold with no quota setting is
+ just ignored. */
+
+ if (tolower(*rest) == 'k') { d *= 1024.0; rest++; }
+ else if (tolower(*rest) == 'm') { d *= 1024.0*1024.0; rest++; }
+ else if (tolower(*rest) == 'g') { d *= 1024.0*1024.0*1024.0; rest++; }
+ else if (*rest == '%' && i == 2)
+ {
+ if (ob->quota_value <= 0 && !ob->maildir_use_size_file)
+ d = 0;
+ else if ((int)d < 0 || (int)d > 100)
+ {
+ *errmsg = string_sprintf("Invalid quota_warn_threshold percentage (%d)"
+ " for %s transport", (int)d, tblock->name);
+ return FAIL;
+ }
+ ob->quota_warn_threshold_is_percent = TRUE;
+ rest++;
+ }
+
+
+ /* For quota and quota_filecount there may be options
+ appended. Currently only "no_check", so we can be lazy parsing it */
+ if (i < 2 && Ustrstr(rest, "/no_check") == rest)
+ {
+ no_check = 1;
+ rest += sizeof("/no_check") - 1;
+ }
+
+ Uskip_whitespace(&rest);
+
+ if (*rest)
+ {
+ *errmsg = string_sprintf("Malformed value \"%s\" (expansion of \"%s\") "
+ "in %s transport", s, q, tblock->name);
+ return FAIL;
+ }
+ }
+
+ /* Set each value, checking for possible overflow. */
+
+ switch (i)
+ {
+ case 0:
+ if (d >= 2.0*1024.0*1024.0*1024.0 && sizeof(off_t) <= 4)
+ which = US"quota";
+ ob->quota_value = (off_t)d;
+ ob->quota_no_check = no_check;
+ q = ob->quota_filecount;
+ break;
+
+ case 1:
+ if (d >= 2.0*1024.0*1024.0*1024.0)
+ which = US"quota_filecount";
+ ob->quota_filecount_value = (int)d;
+ ob->quota_filecount_no_check = no_check;
+ q = ob->quota_warn_threshold;
+ break;
+
+ case 2:
+ if (d >= 2.0*1024.0*1024.0*1024.0 && sizeof(off_t) <= 4)
+ which = US"quota_warn_threshold";
+ ob->quota_warn_threshold_value = (off_t)d;
+ q = ob->mailbox_size_string;
+ default_value = -1.0;
+ break;
+
+ case 3:
+ if (d >= 2.0*1024.0*1024.0*1024.0 && sizeof(off_t) <= 4)
+ which = US"mailbox_size";;
+ ob->mailbox_size_value = (off_t)d;
+ q = ob->mailbox_filecount_string;
+ break;
+
+ case 4:
+ if (d >= 2.0*1024.0*1024.0*1024.0)
+ which = US"mailbox_filecount";
+ ob->mailbox_filecount_value = (int)d;
+ break;
+ }
+
+ if (which)
+ {
+ *errmsg = string_sprintf("%s value %.10g is too large (overflow) in "
+ "%s transport", which, d, tblock->name);
+ return FAIL;
+ }
+ }
+
+return OK;
+}
+
+
+
+/*************************************************
+* 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
+appendfile_transport_init(transport_instance *tblock)
+{
+appendfile_transport_options_block *ob =
+ (appendfile_transport_options_block *)(tblock->options_block);
+uschar * s;
+
+/* Set up the setup entry point, to be called in the privileged state */
+
+tblock->setup = appendfile_transport_setup;
+
+/* Lock_retries must be greater than zero */
+
+if (ob->lock_retries == 0) ob->lock_retries = 1;
+
+/* Only one of a file name or directory name must be given. */
+
+if (ob->filename && ob->dirname)
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s transport:\n "
+ "only one of \"file\" or \"directory\" can be specified", tblock->name);
+
+/* If a file name was specified, neither quota_filecount nor quota_directory
+must be given. */
+
+if (ob->filename)
+ {
+ if (ob->quota_filecount)
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s transport:\n "
+ "quota_filecount must not be set without \"directory\"", tblock->name);
+ if (ob->quota_directory)
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s transport:\n "
+ "quota_directory must not be set without \"directory\"", tblock->name);
+ }
+
+/* The default locking depends on whether MBX is set or not. Change the
+built-in default if none of the lock options has been explicitly set. At least
+one form of locking is required in all cases, but mbx locking changes the
+meaning of fcntl and flock locking. */
+
+/* Not all operating systems provide flock(). For those that do, if flock is
+requested, the default for fcntl is FALSE. */
+
+if (ob->use_flock)
+ {
+ #ifdef NO_FLOCK
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s transport:\n "
+ "flock() support was not available in the operating system when this "
+ "binary was built", tblock->name);
+ #endif /* NO_FLOCK */
+ if (!ob->set_use_fcntl) ob->use_fcntl = FALSE;
+ }
+
+#ifdef SUPPORT_MBX
+if (ob->mbx_format)
+ if (!ob->set_use_lockfile && !ob->set_use_fcntl && !ob->set_use_flock &&
+ !ob->set_use_mbx_lock)
+ {
+ ob->use_lockfile = ob->use_flock = FALSE;
+ ob->use_mbx_lock = ob->use_fcntl = TRUE;
+ }
+ else if (ob->use_mbx_lock)
+ {
+ if (!ob->set_use_lockfile) ob->use_lockfile = FALSE;
+ if (!ob->set_use_fcntl) ob->use_fcntl = FALSE;
+ if (!ob->set_use_flock) ob->use_flock = FALSE;
+ if (!ob->use_fcntl && !ob->use_flock) ob->use_fcntl = TRUE;
+ }
+#endif /* SUPPORT_MBX */
+
+if (!ob->use_fcntl && !ob->use_flock && !ob->use_lockfile && !ob->use_mbx_lock)
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s transport:\n "
+ "no locking configured", tblock->name);
+
+/* Unset timeouts for non-used locking types */
+
+if (!ob->use_fcntl) ob->lock_fcntl_timeout = 0;
+if (!ob->use_flock) ob->lock_flock_timeout = 0;
+
+/* If a directory name was specified, only one of maildir or mailstore may be
+specified, and if quota_filecount or quota_directory is given, quota must
+be set. */
+
+if (ob->dirname)
+ {
+ if (ob->maildir_format && ob->mailstore_format)
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s transport:\n "
+ "only one of maildir and mailstore may be specified", tblock->name);
+ if (ob->quota_filecount != NULL && ob->quota == NULL)
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s transport:\n "
+ "quota must be set if quota_filecount is set", tblock->name);
+ if (ob->quota_directory != NULL && ob->quota == NULL)
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s transport:\n "
+ "quota must be set if quota_directory is set", tblock->name);
+ }
+
+/* If a fixed uid field is set, then a gid field must also be set. */
+
+if (tblock->uid_set && !tblock->gid_set && !tblock->expand_gid)
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG,
+ "user set without group for the %s transport", tblock->name);
+
+/* If "create_file" is set, check that a valid option is given, and set the
+integer variable. */
+
+if ((s = ob->create_file_string ) && *s)
+ {
+ int val = 0;
+ if (Ustrcmp(s, "anywhere") == 0) val = create_anywhere;
+ else if (*s == '/' || Ustrcmp(s, "belowhome") == 0) val = create_belowhome;
+ else if (Ustrcmp(s, "inhome") == 0) val = create_inhome;
+ else
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG,
+ "invalid value given for \"create_file\" for the %s transport: '%s'",
+ tblock->name, s);
+ ob->create_file = val;
+ }
+
+/* If quota_warn_threshold is set, set up default for warn_message. It may
+not be used if the actual threshold for a given delivery ends up as zero,
+of if it's given as a percentage and there's no quota setting. */
+
+if (ob->quota_warn_threshold)
+ {
+ if (!tblock->warn_message) tblock->warn_message = US
+ "To: $local_part@$domain\n"
+ "Subject: Your mailbox\n\n"
+ "This message is automatically created by mail delivery software (Exim).\n\n"
+ "The size of your mailbox has exceeded a warning threshold that is\n"
+ "set by the system administrator.\n";
+ }
+
+/* If batch SMTP is set, force the check and escape strings, and arrange that
+headers are also escaped. */
+
+if (ob->use_bsmtp)
+ {
+ ob->check_string = US".";
+ ob->escape_string = US"..";
+ ob->options |= topt_escape_headers;
+ }
+
+/* If not batch SMTP, not maildir, not mailstore, and directory is not set,
+insert default values for for the affixes and the check/escape strings. */
+
+else if (!ob->dirname && !ob->maildir_format && !ob->mailstore_format)
+ {
+ if (!ob->message_prefix) ob->message_prefix =
+ US"From ${if def:return_path{$return_path}{MAILER-DAEMON}} ${tod_bsdinbox}\n";
+ if (!ob->message_suffix) ob->message_suffix = US"\n";
+ if (!ob->check_string) ob->check_string = US"From ";
+ if (!ob->escape_string) ob->escape_string = US">From ";
+
+ }
+
+/* Set up the bitwise options for transport_write_message from the various
+driver options. Only one of body_only and headers_only can be set. */
+
+ob->options |=
+ (tblock->body_only ? topt_no_headers : 0) |
+ (tblock->headers_only ? topt_no_body : 0) |
+ (tblock->return_path_add ? topt_add_return_path : 0) |
+ (tblock->delivery_date_add ? topt_add_delivery_date : 0) |
+ (tblock->envelope_to_add ? topt_add_envelope_to : 0) |
+ ((ob->use_crlf || ob->mbx_format) ? topt_use_crlf : 0);
+}
+
+
+
+/*************************************************
+* Notify comsat *
+*************************************************/
+
+/* The comsat daemon is the thing that provides asynchronous notification of
+the arrival of local messages, if requested by the user by "biff y". It is a
+BSD thing that uses a TCP/IP protocol for communication. A message consisting
+of the text "user@offset" must be sent, where offset is the place in the
+mailbox where new mail starts. There is no scope for telling it which file to
+look at, which makes it a less than useful if mail is being delivered into a
+non-standard place such as the user's home directory. In fact, it doesn't seem
+to pay much attention to the offset.
+
+Arguments:
+ user user name
+ offset offset in mailbox
+
+Returns: nothing
+*/
+
+static void
+notify_comsat(uschar *user, off_t offset)
+{
+struct servent *sp;
+host_item host;
+uschar * s;
+
+DEBUG(D_transport) debug_printf("notify_comsat called\n");
+
+s = string_sprintf("%.200s@" OFF_T_FMT "\n", user, offset);
+
+if ((sp = getservbyname("biff", "udp")) == NULL)
+ {
+ DEBUG(D_transport) debug_printf("biff/udp is an unknown service");
+ return;
+ }
+
+host.name = US"localhost";
+host.next = NULL;
+
+
+/* This code is all set up to look up "localhost" and use all its addresses
+until one succeeds. However, it appears that at least on some systems, comsat
+doesn't listen on the ::1 address. So for the moment, just force the address to
+be 127.0.0.1. At some future stage, when IPv6 really is superseding IPv4, this
+can be changed. (But actually, comsat is probably dying out anyway.) */
+
+/******
+if (host_find_byname(&host, NULL, 0, NULL, FALSE) == HOST_FIND_FAILED)
+ {
+ DEBUG(D_transport) debug_printf("\"localhost\" unknown\n");
+ return;
+ }
+******/
+
+host.address = US"127.0.0.1";
+
+
+for (host_item * h = &host; h; h = h->next)
+ {
+ int sock, rc;
+ int host_af = Ustrchr(h->address, ':') != NULL ? AF_INET6 : AF_INET;
+
+ DEBUG(D_transport) debug_printf("calling comsat on %s\n", h->address);
+
+ if ((sock = ip_socket(SOCK_DGRAM, host_af)) < 0) continue;
+
+ /* Connect never fails for a UDP socket, so don't set a timeout. */
+
+ (void)ip_connect(sock, host_af, h->address, ntohs(sp->s_port), 0, NULL);
+ rc = send(sock, s, Ustrlen(s) + 1, 0);
+ (void)close(sock);
+
+ if (rc >= 0) break;
+ DEBUG(D_transport)
+ debug_printf("send to comsat failed for %s: %s\n", strerror(errno),
+ h->address);
+ }
+}
+
+
+
+/*************************************************
+* Check the format of a file *
+*************************************************/
+
+/* This function is called when file_format is set, to check that an existing
+file has the right format. The format string contains text/transport pairs. The
+string matching is literal. we just read big_buffer_size bytes, because this is
+all about the first few bytes of a file.
+
+Arguments:
+ cfd the open file
+ tblock the transport block
+ addr the address block - for inserting error data
+
+Returns: pointer to the required transport, or NULL
+*/
+
+transport_instance *
+check_file_format(int cfd, transport_instance *tblock, address_item *addr)
+{
+const uschar *format =
+ ((appendfile_transport_options_block *)(tblock->options_block))->file_format;
+uschar data[256];
+int len = read(cfd, data, sizeof(data));
+int sep = 0;
+uschar *s;
+
+DEBUG(D_transport) debug_printf("checking file format\n");
+
+/* An empty file matches the current transport */
+
+if (len == 0) return tblock;
+
+/* Search the formats for a match */
+
+/* not expanded so cannot be tainted */
+while ((s = string_nextinlist(&format, &sep, big_buffer, big_buffer_size)))
+ {
+ int slen = Ustrlen(s);
+ BOOL match = len >= slen && Ustrncmp(data, s, slen) == 0;
+ uschar *tp = string_nextinlist(&format, &sep, big_buffer, big_buffer_size);
+
+ if (match && tp)
+ {
+ for (transport_instance * tt = transports; tt; tt = tt->next)
+ if (Ustrcmp(tp, tt->name) == 0)
+ {
+ DEBUG(D_transport)
+ debug_printf("file format -> %s transport\n", tt->name);
+ return tt;
+ }
+ addr->basic_errno = ERRNO_BADTRANSPORT;
+ addr->message = string_sprintf("%s transport (for %.*s format) not found",
+ tp, slen, data);
+ return NULL;
+ }
+ }
+
+/* Failed to find a match */
+
+addr->basic_errno = ERRNO_FORMATUNKNOWN;
+addr->message = US"mailbox file format unrecognized";
+return NULL;
+}
+
+
+
+
+/*************************************************
+* Check directory's files for quota *
+*************************************************/
+
+/* This function is called if quota is set for one of the delivery modes that
+delivers into a specific directory. It scans the directory and stats all the
+files in order to get a total size and count. This is an expensive thing to do,
+but some people are prepared to bear the cost. Alternatively, if size_regex is
+set, it is used as a regex to try to extract the size from the file name, a
+strategy that some people use on maildir files on systems where the users have
+no shell access.
+
+The function is global, because it is also called from tf_maildir.c for maildir
+folders (which should contain only regular files).
+
+Note: Any problems can be written to debugging output, but cannot be written to
+the log, because we are running as an unprivileged user here.
+
+Arguments:
+ dirname the name of the directory
+ countptr where to add the file count (because this function recurses)
+ re a compiled regex to get the size from a name
+
+Returns: the sum of the sizes of the stattable files
+ zero if the directory cannot be opened
+*/
+
+off_t
+check_dir_size(const uschar * dirname, int * countptr, const pcre2_code * re)
+{
+DIR *dir;
+off_t sum = 0;
+int count = *countptr;
+
+if (!(dir = exim_opendir(dirname))) return 0;
+
+for (struct dirent *ent; ent = readdir(dir); )
+ {
+ uschar * path, * name = US ent->d_name;
+ struct stat statbuf;
+
+ if (Ustrcmp(name, ".") == 0 || Ustrcmp(name, "..") == 0) continue;
+
+ count++;
+
+ /* If there's a regex, try to find the size using it */
+
+ if (re)
+ {
+ pcre2_match_data * md = pcre2_match_data_create(2, pcre_gen_ctx);
+ int rc = pcre2_match(re, (PCRE2_SPTR)name, PCRE2_ZERO_TERMINATED,
+ 0, 0, md, pcre_mtc_ctx);
+ PCRE2_SIZE * ovec = pcre2_get_ovector_pointer(md);
+ if ( rc >= 0
+ && (rc = pcre2_get_ovector_count(md)) >= 2)
+ {
+ uschar *endptr;
+ off_t size = (off_t)Ustrtod(name + ovec[2], &endptr);
+ if (endptr == name + ovec[3])
+ {
+ sum += size;
+ DEBUG(D_transport)
+ debug_printf("check_dir_size: size from %s is " OFF_T_FMT "\n", name,
+ size);
+ continue;
+ }
+ }
+ DEBUG(D_transport)
+ debug_printf("check_dir_size: regex did not match %s\n", name);
+ }
+
+ /* No regex or no match for the regex, or captured non-digits */
+
+ path = string_sprintf("%s/%s", dirname, name);
+
+ if (Ustat(path, &statbuf) < 0)
+ {
+ DEBUG(D_transport)
+ debug_printf("check_dir_size: stat error %d for %s: %s\n", errno, path,
+ strerror(errno));
+ }
+ else
+ if ((statbuf.st_mode & S_IFMT) == S_IFREG)
+ sum += statbuf.st_size / statbuf.st_nlink;
+ else if ((statbuf.st_mode & S_IFMT) == S_IFDIR)
+ sum += check_dir_size(path, &count, re);
+ }
+
+closedir(dir);
+DEBUG(D_transport)
+ debug_printf("check_dir_size: dir=%s sum=" OFF_T_FMT " count=%d\n", dirname,
+ sum, count);
+
+*countptr = count;
+return sum;
+}
+
+
+
+
+/*************************************************
+* Apply a lock to a file descriptor *
+*************************************************/
+
+/* This function applies a lock to a file descriptor, using a blocking or
+non-blocking lock, depending on the timeout value. It can apply either or
+both of a fcntl() and a flock() lock. However, not all OS support flock();
+for those that don't, the use_flock option cannot be set.
+
+Arguments:
+ fd the file descriptor
+ fcntltype type of lock, specified as F_WRLCK or F_RDLCK (that is, in
+ fcntl() format); the flock() type is deduced if needed
+ dofcntl do fcntl() locking
+ fcntltime non-zero to use blocking fcntl()
+ doflock do flock() locking
+ flocktime non-zero to use blocking flock()
+
+Returns: yield of the fcntl() or flock() call, with errno preserved;
+ sigalrm_seen set if there has been a timeout
+*/
+
+static int
+apply_lock(int fd, int fcntltype, BOOL dofcntl, int fcntltime, BOOL doflock,
+ int flocktime)
+{
+int yield = 0;
+int save_errno;
+struct flock lock_data;
+lock_data.l_type = fcntltype;
+lock_data.l_whence = lock_data.l_start = lock_data.l_len = 0;
+
+sigalrm_seen = FALSE;
+
+if (dofcntl)
+ {
+ if (fcntltime > 0)
+ {
+ ALARM(fcntltime);
+ yield = fcntl(fd, F_SETLKW, &lock_data);
+ save_errno = errno;
+ ALARM_CLR(0);
+ errno = save_errno;
+ }
+ else yield = fcntl(fd, F_SETLK, &lock_data);
+ }
+
+#ifndef NO_FLOCK
+if (doflock && (yield >= 0))
+ {
+ int flocktype = (fcntltype == F_WRLCK) ? LOCK_EX : LOCK_SH;
+ if (flocktime > 0)
+ {
+ ALARM(flocktime);
+ yield = flock(fd, flocktype);
+ save_errno = errno;
+ ALARM_CLR(0);
+ errno = save_errno;
+ }
+ else yield = flock(fd, flocktype | LOCK_NB);
+ }
+#endif /* NO_FLOCK */
+
+return yield;
+}
+
+
+
+
+#ifdef SUPPORT_MBX
+/*************************************************
+* Copy message into MBX mailbox *
+*************************************************/
+
+/* This function is called when a message intended for a MBX mailbox has been
+written to a temporary file. We can now get the size of the message and then
+copy it in MBX format to the mailbox.
+
+Arguments:
+ to_fd fd to write to (the real mailbox)
+ from_fd fd to read from (the temporary file)
+ saved_size current size of mailbox
+
+Returns: OK if all went well, DEFER otherwise, with errno preserved
+ the number of bytes written are added to transport_count
+ by virtue of calling transport_write_block()
+*/
+
+/* Values taken from c-client */
+
+#define MBX_HDRSIZE 2048
+#define MBX_NUSERFLAGS 30
+
+static int
+copy_mbx_message(int to_fd, int from_fd, off_t saved_size)
+{
+int used;
+off_t size;
+struct stat statbuf;
+transport_ctx tctx = { .u={.fd = to_fd}, .options = topt_not_socket };
+
+/* If the current mailbox size is zero, write a header block */
+
+if (saved_size == 0)
+ {
+ uschar *s;
+ memset (deliver_out_buffer, '\0', MBX_HDRSIZE);
+ sprintf(CS(s = deliver_out_buffer), "*mbx*\015\012%08lx00000000\015\012",
+ (long int)time(NULL));
+ for (int i = 0; i < MBX_NUSERFLAGS; i++)
+ sprintf (CS(s += Ustrlen(s)), "\015\012");
+ if (!transport_write_block (&tctx, deliver_out_buffer, MBX_HDRSIZE, FALSE))
+ return DEFER;
+ }
+
+DEBUG(D_transport) debug_printf("copying MBX message from temporary file\n");
+
+/* Now construct the message's header from the time and the RFC822 file
+size, including CRLFs, which is the size of the input (temporary) file. */
+
+if (fstat(from_fd, &statbuf) < 0) return DEFER;
+size = statbuf.st_size;
+
+sprintf (CS deliver_out_buffer, "%s," OFF_T_FMT ";%08lx%04x-%08x\015\012",
+ tod_stamp(tod_mbx), size, 0L, 0, 0);
+used = Ustrlen(deliver_out_buffer);
+
+/* Rewind the temporary file, and copy it over in chunks. */
+
+if (lseek(from_fd, 0 , SEEK_SET) < 0) return DEFER;
+
+while (size > 0)
+ {
+ int len = read(from_fd, deliver_out_buffer + used,
+ DELIVER_OUT_BUFFER_SIZE - used);
+ if (len <= 0)
+ {
+ if (len == 0) errno = ERRNO_MBXLENGTH;
+ return DEFER;
+ }
+ if (!transport_write_block(&tctx, deliver_out_buffer, used + len, FALSE))
+ return DEFER;
+ size -= len;
+ used = 0;
+ }
+
+return OK;
+}
+#endif /* SUPPORT_MBX */
+
+
+
+/*************************************************
+* Check creation is permitted *
+*************************************************/
+
+/* This function checks whether a given file name is permitted to be created,
+as controlled by the create_file option. If no home directory is set, however,
+we can't do any tests.
+
+Arguments:
+ filename the file name
+ create_file the ob->create_file option
+ deliver_dir the delivery directory
+
+Returns: TRUE if creation is permitted
+*/
+
+static BOOL
+check_creation(uschar *filename, int create_file, const uschar * deliver_dir)
+{
+BOOL yield = TRUE;
+
+if (deliver_dir && create_file != create_anywhere)
+ {
+ int len = Ustrlen(deliver_dir);
+ uschar *file = filename;
+
+ while (file[0] == '/' && file[1] == '/') file++;
+ if ( Ustrncmp(file, deliver_dir, len) != 0
+ || file[len] != '/'
+ || Ustrchr(file+len+2, '/') != NULL
+ && ( create_file != create_belowhome
+ || Ustrstr(file+len, "/../") != NULL
+ )
+ ) yield = FALSE;
+
+ /* If yield is TRUE, the file name starts with the home directory, and does
+ not contain any instances of "/../" in the "belowhome" case. However, it may
+ still contain symbolic links. We can check for this by making use of
+ realpath(), which most Unixes seem to have (but make it possible to cut this
+ out). We can't just use realpath() on the whole file name, because we know
+ the file itself doesn't exist, and intermediate directories may also not
+ exist. What we want to know is the real path of the longest existing part of
+ the path. That must match the home directory's beginning, whichever is the
+ shorter. */
+
+ #ifndef NO_REALPATH
+ if (yield && create_file == create_belowhome)
+ {
+ uschar *next;
+ uschar *rp = NULL;
+ for (uschar * slash = Ustrrchr(file, '/'); /* There is known to be one */
+ !rp && slash > file; /* Stop if reached beginning */
+ slash = next)
+ {
+ *slash = 0;
+ rp = US realpath(CS file, CS big_buffer);
+ next = Ustrrchr(file, '/');
+ *slash = '/';
+ }
+
+ /* If rp == NULL it means that none of the relevant directories exist.
+ This is not a problem here - it means that no symbolic links can exist,
+ which is all we are worried about. Otherwise, we must compare it
+ against the start of the home directory. However, that may itself
+ contain symbolic links, so we have to "realpath" it as well, if
+ possible. */
+
+ if (rp)
+ {
+ uschar hdbuffer[PATH_MAX+1];
+ const uschar * rph = deliver_dir;
+ int rlen = Ustrlen(big_buffer);
+
+ if ((rp = US realpath(CS deliver_dir, CS hdbuffer)))
+ {
+ rph = hdbuffer;
+ len = Ustrlen(rph);
+ }
+
+ if (rlen > len) rlen = len;
+ if (Ustrncmp(rph, big_buffer, rlen) != 0)
+ {
+ yield = FALSE;
+ DEBUG(D_transport) debug_printf("Real path \"%s\" does not match \"%s\"\n",
+ big_buffer, deliver_dir);
+ }
+ }
+ }
+ #endif /* NO_REALPATH */
+ }
+
+return yield;
+}
+
+
+
+/*************************************************
+* Main entry point *
+*************************************************/
+
+/* See local README for general interface details. This transport always
+returns FALSE, indicating that the status which has been placed in the first
+address should be copied to any other addresses in a batch.
+
+Appendfile delivery is tricky and has led to various security problems in other
+mailers. The logic used here is therefore laid out in some detail. When this
+function is called, we are running in a subprocess which has had its gid and
+uid set to the appropriate values. Therefore, we cannot write directly to the
+exim logs. Any errors must be handled by setting appropriate return codes.
+Note that the default setting for addr->transport_return is DEFER, so it need
+not be set unless some other value is required.
+
+The code below calls geteuid() rather than getuid() to get the current uid
+because in weird configurations not running setuid root there may be a
+difference. In the standard configuration, where setuid() has been used in the
+delivery process, there will be no difference between the uid and the euid.
+
+(1) If the af_file flag is set, this is a delivery to a file after .forward or
+ alias expansion. Otherwise, there must be a configured file name or
+ directory name.
+
+The following items apply in the case when a file name (as opposed to a
+directory name) is given, that is, when appending to a single file:
+
+(2f) Expand the file name.
+
+(3f) If the file name is /dev/null, return success (optimization).
+
+(4f) If the file_format options is set, open the file for reading, and check
+ that the bytes at the start of the file match one of the given strings.
+ If the check indicates a transport other than the current one should be
+ used, pass control to that other transport. Otherwise continue. An empty
+ or non-existent file matches the current transport. The file is closed
+ after the check.
+
+(5f) If a lock file is required, create it (see extensive separate comments
+ below about the algorithm for doing this). It is important to do this
+ before opening the mailbox if NFS is in use.
+
+(6f) Stat the file, using lstat() rather than stat(), in order to pick up
+ details of any symbolic link.
+
+(7f) If the file already exists:
+
+ Check the owner and group if necessary, and defer if they are wrong.
+
+ If it is a symbolic link AND the allow_symlink option is set (NOT the
+ default), go back to (6f) but this time use stat() instead of lstat().
+
+ If it's not a regular file (or FIFO when permitted), defer delivery.
+
+ Check permissions. If the required permissions are *less* than the
+ existing ones, or supplied by the address (often by the user via filter),
+ chmod() the file. Otherwise, defer.
+
+ Save the inode number.
+
+ Open with O_RDRW + O_APPEND, thus failing if the file has vanished.
+
+ If open fails because the file does not exist, go to (6f); on any other
+ failure, defer.
+
+ Check the inode number hasn't changed - I realize this isn't perfect (an
+ inode can be reused) but it's cheap and will catch some of the races.
+
+ Check it's still a regular file (or FIFO if permitted).
+
+ Check that the owner and permissions haven't changed.
+
+ If file_format is set, check that the file still matches the format for
+ the current transport. If not, defer delivery.
+
+(8f) If file does not exist initially:
+
+ Open with O_WRONLY + O_EXCL + O_CREAT with configured mode, unless we know
+ this is via a symbolic link (only possible if allow_symlinks is set), in
+ which case don't use O_EXCL, as it doesn't work.
+
+ If open fails because the file already exists, go to (6f). To avoid
+ looping for ever in a situation where the file is continuously being
+ created and deleted, all of this happens inside a loop that operates
+ lock_retries times and includes the fcntl and flock locking. If the
+ loop completes without the file getting opened, defer and request
+ freezing, because something really weird is happening.
+
+ If open fails for any other reason, defer for subsequent delivery except
+ when this is a file delivery resulting from an alias or forward expansion
+ and the error is EPERM or ENOENT or EACCES, in which case FAIL as this is
+ most likely a user rather than a configuration error.
+
+(9f) We now have the file checked and open for writing. If so configured, lock
+ it using fcntl, flock, or MBX locking rules. If this fails, close the file
+ and goto (6f), up to lock_retries times, after sleeping for a while. If it
+ still fails, give up and defer delivery.
+
+(10f)Save the access time (for subsequent restoration) and the size of the
+ file, for comsat and for re-setting if delivery fails in the middle -
+ e.g. for quota exceeded.
+
+The following items apply in the case when a directory name is given:
+
+(2d) Create a new file in the directory using a temporary name, by opening for
+ writing and with O_CREAT. If maildir format is being used, the file
+ is created in a temporary subdirectory with a prescribed name. If
+ mailstore format is being used, the envelope file is first created with a
+ temporary name, then the data file.
+
+The following items apply in all cases:
+
+(11) We now have the file open for writing, and locked if it was given as a
+ file name. Write the message and flush the file, unless there is a setting
+ of the local quota option, in which case we can check for its excession
+ without doing any writing.
+
+ In the case of MBX format mailboxes, the message is first written to a
+ temporary file, in order to get its correct length. This is then copied to
+ the real file, preceded by an MBX header.
+
+ If there is a quota error on writing, defer the address. Timeout logic
+ will determine for how long retries are attempted. We restore the mailbox
+ to its original length if it's a single file. There doesn't seem to be a
+ uniform error code for quota excession (it even differs between SunOS4
+ and some versions of SunOS5) so a system-dependent macro called
+ ERRNO_QUOTA is used for it, and the value gets put into errno_quota at
+ compile time.
+
+ For any other error (most commonly disk full), do the same.
+
+The following applies after appending to a file:
+
+(12f)Restore the atime; notify_comsat if required; close the file (which
+ unlocks it if it was locked). Delete the lock file if it exists.
+
+The following applies after writing a unique file in a directory:
+
+(12d)For maildir format, rename the file into the new directory. For mailstore
+ format, rename the envelope file to its correct name. Otherwise, generate
+ a unique name from the directory_file option, and rename to that, possibly
+ trying a few times if the file exists and re-expanding the name gives a
+ different string.
+
+This transport yields FAIL only when a file name is generated by an alias or
+forwarding operation and attempting to open it gives EPERM, ENOENT, or EACCES.
+All other failures return DEFER (in addr->transport_return). */
+
+
+BOOL
+appendfile_transport_entry(
+ transport_instance *tblock, /* data for this instantiation */
+ address_item *addr) /* address we are working on */
+{
+appendfile_transport_options_block *ob =
+ (appendfile_transport_options_block *)(tblock->options_block);
+struct stat statbuf;
+const uschar * deliver_dir;
+uschar *fdname = NULL;
+uschar *filename = NULL;
+uschar *hitchname = NULL;
+uschar *dataname = NULL;
+uschar *lockname = NULL;
+uschar *newname = NULL;
+uschar *nametag = NULL;
+uschar *cr = US"";
+uschar *filecount_msg = US"";
+uschar *path;
+struct utimbuf times;
+struct timeval msg_tv;
+BOOL disable_quota = FALSE;
+BOOL isdirectory = FALSE;
+BOOL isfifo = FALSE;
+BOOL wait_for_tick = FALSE;
+uid_t uid = geteuid(); /* See note above */
+gid_t gid = getegid();
+int mbformat;
+int mode = (addr->mode > 0) ? addr->mode : ob->mode;
+off_t saved_size = -1;
+off_t mailbox_size = ob->mailbox_size_value;
+int mailbox_filecount = ob->mailbox_filecount_value;
+int hd = -1;
+int fd = -1;
+int yield = FAIL;
+int i;
+
+#ifdef SUPPORT_MBX
+int save_fd = 0;
+int mbx_lockfd = -1;
+uschar mbx_lockname[40];
+FILE *temp_file = NULL;
+#endif /* SUPPORT_MBX */
+
+#ifdef SUPPORT_MAILDIR
+int maildirsize_fd = -1; /* fd for maildirsize file */
+int maildir_save_errno;
+#endif
+
+
+DEBUG(D_transport) debug_printf("appendfile transport entered\n");
+
+/* An "address_file" or "address_directory" transport is used to deliver to
+files specified via .forward or an alias file. Prior to release 4.20, the
+"file" and "directory" options were ignored in this case. This has been changed
+to allow the redirection data to specify what is in effect a folder, whose
+location is determined by the options on the transport.
+
+Compatibility with the case when neither option is set is retained by forcing a
+value for the file or directory name. A directory delivery is assumed if the
+last character of the path from the router is '/'.
+
+The file path is in the local part of the address, but not in the $local_part
+variable (that holds the parent local part). It is, however, in the
+$address_file variable. Below, we update the local part in the address if it
+changes by expansion, so that the final path ends up in the log. */
+
+if (testflag(addr, af_file) && !ob->filename && !ob->dirname)
+ {
+ fdname = US"$address_file";
+ if (address_file[Ustrlen(address_file)-1] == '/' ||
+ ob->maildir_format ||
+ ob->mailstore_format)
+ isdirectory = TRUE;
+ }
+
+/* Handle (a) an "address file" delivery where "file" or "directory" is
+explicitly set and (b) a non-address_file delivery, where one of "file" or
+"directory" must be set; initialization ensures that they are not both set. */
+
+if (!fdname)
+ {
+ if (!(fdname = ob->filename))
+ {
+ fdname = ob->dirname;
+ isdirectory = TRUE;
+ }
+ if (!fdname)
+ {
+ addr->message = string_sprintf("Mandatory file or directory option "
+ "missing from %s transport", tblock->name);
+ goto ret_panic;
+ }
+ }
+
+/* Maildir and mailstore require a directory */
+
+if ((ob->maildir_format || ob->mailstore_format) && !isdirectory)
+ {
+ addr->message = string_sprintf("mail%s_format requires \"directory\" "
+ "to be specified for the %s transport",
+ ob->maildir_format ? "dir" : "store", tblock->name);
+ goto ret_panic;
+ }
+
+if (!(path = expand_string(fdname)))
+ {
+ addr->message = string_sprintf("Expansion of \"%s\" (file or directory "
+ "name for %s transport) failed: %s", fdname, tblock->name,
+ expand_string_message);
+ goto ret_panic;
+ }
+
+if (path[0] != '/')
+ {
+ addr->message = string_sprintf("appendfile: file or directory name "
+ "\"%s\" is not absolute", path);
+ addr->basic_errno = ERRNO_NOTABSOLUTE;
+ return FALSE;
+ }
+
+/* For a file delivery, make sure the local part in the address(es) is updated
+to the true local part. */
+
+if (testflag(addr, af_file))
+ for (address_item * addr2 = addr; addr2; addr2 = addr2->next)
+ addr2->local_part = string_copy(path);
+
+/* The available mailbox formats depend on whether it is a directory or a file
+delivery. */
+
+if (isdirectory)
+ {
+ mbformat =
+ #ifdef SUPPORT_MAILDIR
+ ob->maildir_format ? mbf_maildir :
+ #endif
+ #ifdef SUPPORT_MAILSTORE
+ ob->mailstore_format ? mbf_mailstore :
+ #endif
+ mbf_smail;
+ }
+else
+ {
+ mbformat =
+ #ifdef SUPPORT_MBX
+ ob->mbx_format ? mbf_mbx :
+ #endif
+ mbf_unix;
+ }
+
+DEBUG(D_transport)
+ {
+ debug_printf("appendfile: mode=%o notify_comsat=%d quota=" OFF_T_FMT
+ "%s%s"
+ " warning=" OFF_T_FMT "%s\n"
+ " %s=%s format=%s\n message_prefix=%s\n message_suffix=%s\n "
+ "maildir_use_size_file=%s\n",
+ mode, ob->notify_comsat, ob->quota_value,
+ ob->quota_no_check ? " (no_check)" : "",
+ ob->quota_filecount_no_check ? " (no_check_filecount)" : "",
+ ob->quota_warn_threshold_value,
+ ob->quota_warn_threshold_is_percent ? "%" : "",
+ isdirectory ? "directory" : "file",
+ path, mailbox_formats[mbformat],
+ !ob->message_prefix ? US"null" : string_printing(ob->message_prefix),
+ !ob->message_suffix ? US"null" : string_printing(ob->message_suffix),
+ ob->maildir_use_size_file ? "yes" : "no");
+
+ if (!isdirectory) debug_printf(" locking by %s%s%s%s%s\n",
+ ob->use_lockfile ? "lockfile " : "",
+ ob->use_mbx_lock ? "mbx locking (" : "",
+ ob->use_fcntl ? "fcntl " : "",
+ ob->use_flock ? "flock" : "",
+ ob->use_mbx_lock ? ")" : "");
+ }
+
+/* If the -N option is set, can't do any more. */
+
+if (f.dont_deliver)
+ {
+ DEBUG(D_transport)
+ debug_printf("*** delivery by %s transport bypassed by -N option\n",
+ tblock->name);
+ addr->transport_return = OK;
+ return FALSE;
+ }
+
+/* If an absolute path was given for create_file the it overrides deliver_home
+(here) and de-taints the filename (below, after check_creation() */
+
+deliver_dir = *ob->create_file_string == '/'
+ ? ob->create_file_string : deliver_home;
+
+/* Handle the case of a file name. If the file name is /dev/null, we can save
+ourselves some effort and just give a success return right away. */
+
+if (!isdirectory)
+ {
+ BOOL use_lstat = TRUE;
+ BOOL file_opened = FALSE;
+ BOOL allow_creation_here = TRUE;
+
+ if (Ustrcmp(path, "/dev/null") == 0)
+ {
+ addr->transport_return = OK;
+ return FALSE;
+ }
+
+ /* Set the name of the file to be opened, and the file to which the data
+ is written, and find out if we are permitted to create a non-existent file.
+ If the create_file option is an absolute path and the file was within it,
+ de-taint. Chaeck for a tainted path. */
+
+ if ( (allow_creation_here = check_creation(path, ob->create_file, deliver_dir))
+ && ob->create_file == create_belowhome)
+ if (is_tainted(path))
+ {
+ DEBUG(D_transport) debug_printf("de-tainting path '%s'\n", path);
+ path = string_copy_taint(path, GET_UNTAINTED);
+ }
+
+ if (is_tainted(path)) goto tainted_ret_panic;
+ dataname = filename = path;
+
+ /* If ob->create_directory is set, attempt to create the directories in
+ which this mailbox lives, but only if we are permitted to create the file
+ itself. We know we are dealing with an absolute path, because this was
+ checked above. */
+
+ if (ob->create_directory && allow_creation_here)
+ {
+ uschar *p = Ustrrchr(path, '/');
+ p = string_copyn(path, p - path);
+ if (!directory_make(NULL, p, ob->dirmode, FALSE))
+ {
+ addr->basic_errno = errno;
+ addr->message =
+ string_sprintf("failed to create directories for %s: %s", path,
+ exim_errstr(errno));
+ DEBUG(D_transport) debug_printf("%s transport: %s\n", tblock->name, path);
+ return FALSE;
+ }
+ }
+
+ /* If file_format is set we must check that any existing file matches one of
+ the configured formats by checking the bytes it starts with. A match then
+ indicates a specific transport - if it is not this one, pass control to it.
+ Otherwise carry on here. An empty or non-existent file matches the current
+ transport. We don't need to distinguish between non-existence and other open
+ failures because if an existing file fails to open here, it will also fail
+ again later when O_RDWR is used. */
+
+ if (ob->file_format)
+ {
+ int cfd = Uopen(path, O_RDONLY, 0);
+ if (cfd >= 0)
+ {
+ transport_instance *tt = check_file_format(cfd, tblock, addr);
+ (void)close(cfd);
+
+ /* If another transport is indicated, call it and return; if no transport
+ was found, just return - the error data will have been set up.*/
+
+ if (tt != tblock)
+ {
+ if (tt)
+ {
+ set_process_info("delivering %s to %s using %s", message_id,
+ addr->local_part, tt->name);
+ debug_print_string(tt->debug_string);
+ addr->transport = tt;
+ (tt->info->code)(tt, addr);
+ }
+ return FALSE;
+ }
+ }
+ }
+
+ /* The locking of mailbox files is worse than the naming of cats, which is
+ known to be "a difficult matter" (T.S. Eliot) and just as cats must have
+ three different names, so several different styles of locking are used.
+
+ Research in other programs that lock mailboxes shows that there is no
+ universally standard method. Having mailboxes NFS-mounted on the system that
+ is delivering mail is not the best thing, but people do run like this,
+ and so the code must do its best to cope.
+
+ Three different locking mechanisms are supported. The initialization function
+ checks that at least one is configured.
+
+ LOCK FILES
+
+ Unless no_use_lockfile is set, we attempt to build a lock file in a way that
+ will work over NFS. Only after that is done do we actually open the mailbox
+ and apply locks to it (if configured).
+
+ Originally, Exim got the file opened before doing anything about locking.
+ However, a very occasional problem was observed on Solaris 2 when delivering
+ over NFS. It is seems that when a file is opened with O_APPEND, the file size
+ gets remembered at open time. If another process on another host (that's
+ important) has the file open and locked and writes to it and then releases
+ the lock while the first process is waiting to get the lock, the first
+ process may fail to write at the new end point of the file - despite the very
+ definite statement about O_APPEND in the man page for write(). Experiments
+ have reproduced this problem, but I do not know any way of forcing a host to
+ update its attribute cache for an open NFS file. It would be nice if it did
+ so when a lock was taken out, but this does not seem to happen. Anyway, to
+ reduce the risk of this problem happening, we now create the lock file
+ (if configured) *before* opening the mailbox. That will prevent two different
+ Exims opening the file simultaneously. It may not prevent clashes with MUAs,
+ however, but Pine at least seems to operate in the same way.
+
+ Lockfiles should normally be used when NFS is involved, because of the above
+ problem.
+
+ The logic for creating the lock file is:
+
+ . The name of the lock file is <mailbox-name>.lock
+
+ . First, create a "hitching post" name by adding the primary host name,
+ current time and pid to the lock file name. This should be unique.
+
+ . Create the hitching post file using WRONLY + CREAT + EXCL.
+
+ . If that fails EACCES, we assume it means that the user is unable to create
+ files in the mail spool directory. Some installations might operate in this
+ manner, so there is a configuration option to allow this state not to be an
+ error - we proceed to lock using fcntl only, after the file is open.
+
+ . Otherwise, an error causes a deferment of the address.
+
+ . Hard link the hitching post to the lock file name.
+
+ . If the link succeeds, we have successfully created the lock file. Simply
+ close and unlink the hitching post file.
+
+ . If the link does not succeed, proceed as follows:
+
+ o Fstat the hitching post file, and then close and unlink it.
+
+ o Now examine the stat data. If the number of links to the file is exactly
+ 2, the linking succeeded but for some reason, e.g. an NFS server crash,
+ the return never made it back, so the link() function gave a failure
+ return.
+
+ . This method allows for the lock file to be created by some other process
+ right up to the moment of the attempt to hard link it, and is also robust
+ against NFS server crash-reboots, which would probably result in timeouts
+ in the middle of link().
+
+ . System crashes may cause lock files to get left lying around, and some means
+ of flushing them is required. The approach of writing a pid (used by smail
+ and by elm) into the file isn't useful when NFS may be in use. Pine uses a
+ timeout, which seems a better approach. Since any program that writes to a
+ mailbox using a lock file should complete its task very quickly, Pine
+ removes lock files that are older than 5 minutes. We allow the value to be
+ configurable on the transport.
+
+ FCNTL LOCKING
+
+ If use_fcntl_lock is set, then Exim gets an exclusive fcntl() lock on the
+ mailbox once it is open. This is done by default with a non-blocking lock.
+ Failures to lock cause retries after a sleep, but only for a certain number
+ of tries. A blocking lock is deliberately not used so that we don't hold the
+ mailbox open. This minimizes the possibility of the NFS problem described
+ under LOCK FILES above, if for some reason NFS deliveries are happening
+ without lock files. However, the use of a non-blocking lock and sleep, though
+ the safest approach, does not give the best performance on very busy systems.
+ A blocking lock plus timeout does better. Therefore Exim has an option to
+ allow it to work this way. If lock_fcntl_timeout is set greater than zero, it
+ enables the use of blocking fcntl() calls.
+
+ FLOCK LOCKING
+
+ If use_flock_lock is set, then Exim gets an exclusive flock() lock in the
+ same manner as for fcntl locking above. No-blocking/timeout is also set as
+ above in lock_flock_timeout. Not all operating systems provide or support
+ flock(). For those that don't (as determined by the definition of LOCK_SH in
+ /usr/include/sys/file.h), use_flock_lock may not be set. For some OS, flock()
+ is implemented (not precisely) on top of fcntl(), which means there's no
+ point in actually using it.
+
+ MBX LOCKING
+
+ If use_mbx_lock is set (this is supported only if SUPPORT_MBX is defined)
+ then the rules used for locking in c-client are used. Exim takes out a shared
+ lock on the mailbox file, and an exclusive lock on the file whose name is
+ /tmp/.<device-number>.<inode-number>. The shared lock on the mailbox stops
+ any other MBX client from getting an exclusive lock on it and expunging it.
+ It also stops any other MBX client from unlinking the /tmp lock when it has
+ finished with it.
+
+ The exclusive lock on the /tmp file prevents any other MBX client from
+ updating the mailbox in any way. When writing is finished, if an exclusive
+ lock on the mailbox can be obtained, indicating there are no current sharers,
+ the /tmp file is unlinked.
+
+ MBX locking can use either fcntl() or flock() locking. If neither
+ use_fcntl_lock or use_flock_lock is set, it defaults to using fcntl() only.
+ The calls for getting these locks are by default non-blocking, as for non-mbx
+ locking, but can be made blocking by setting lock_fcntl_timeout and/or
+ lock_flock_timeout as appropriate. As MBX delivery doesn't work over NFS, it
+ probably makes sense to set timeouts for any MBX deliveries. */
+
+
+ /* Build a lock file if configured to do so - the existence of a lock
+ file is subsequently checked by looking for a non-negative value of the
+ file descriptor hd - even though the file is no longer open. */
+
+ if (ob->use_lockfile)
+ {
+ /* cf. exim_lock.c */
+ lockname = string_sprintf("%s.lock", filename);
+ hitchname = string_sprintf( "%s.%s.%08x.%08x", lockname, primary_hostname,
+ (unsigned int)(time(NULL)), (unsigned int)getpid());
+
+ DEBUG(D_transport) debug_printf("lock name: %s\nhitch name: %s\n", lockname,
+ hitchname);
+
+ /* Lock file creation retry loop */
+
+ for (i = 0; i < ob->lock_retries; sleep(ob->lock_interval), i++)
+ {
+ int rc;
+
+ hd = Uopen(hitchname, O_WRONLY | O_CREAT | O_EXCL, ob->lockfile_mode);
+ if (hd < 0)
+ {
+ addr->basic_errno = errno;
+ addr->message =
+ string_sprintf("creating lock file hitching post %s "
+ "(euid=%ld egid=%ld)", hitchname, (long int)geteuid(),
+ (long int)getegid());
+ return FALSE;
+ }
+
+ /* Attempt to hitch the hitching post to the lock file. If link()
+ succeeds (the common case, we hope) all is well. Otherwise, fstat the
+ file, and get rid of the hitching post. If the number of links was 2,
+ the link was created, despite the failure of link(). If the hitch was
+ not successful, try again, having unlinked the lock file if it is too
+ old.
+
+ There's a version of Linux (2.0.27) which doesn't update its local cache
+ of the inode after link() by default - which many think is a bug - but
+ if the link succeeds, this code will be OK. It just won't work in the
+ case when link() fails after having actually created the link. The Linux
+ NFS person is fixing this; a temporary patch is available if anyone is
+ sufficiently worried. */
+
+ if ((rc = Ulink(hitchname, lockname)) != 0) fstat(hd, &statbuf);
+ (void)close(hd);
+ Uunlink(hitchname);
+ if (rc != 0 && statbuf.st_nlink != 2)
+ {
+ if (ob->lockfile_timeout > 0 && Ustat(lockname, &statbuf) == 0 &&
+ time(NULL) - statbuf.st_ctime > ob->lockfile_timeout)
+ {
+ DEBUG(D_transport) debug_printf("unlinking timed-out lock file\n");
+ Uunlink(lockname);
+ }
+ DEBUG(D_transport) debug_printf("link of hitching post failed - retrying\n");
+ continue;
+ }
+
+ DEBUG(D_transport) debug_printf("lock file created\n");
+ break;
+ }
+
+ /* Check for too many tries at creating the lock file */
+
+ if (i >= ob->lock_retries)
+ {
+ addr->basic_errno = ERRNO_LOCKFAILED;
+ addr->message = string_sprintf("failed to lock mailbox %s (lock file)",
+ filename);
+ return FALSE;
+ }
+ }
+
+
+ /* We now have to get the file open. First, stat() it and act on existence or
+ non-existence. This is in a loop to handle the case of a file's being created
+ or deleted as we watch, and also to handle retries when the locking fails.
+ Rather than holding the file open while waiting for the fcntl() and/or
+ flock() lock, we close and do the whole thing again. This should be safer,
+ especially for NFS files, which might get altered from other hosts, making
+ their cached sizes incorrect.
+
+ With the default settings, no symlinks are permitted, but there is an option
+ to permit symlinks for those sysadmins that know what they are doing.
+ Shudder. However, insist that the initial symlink is owned by the right user.
+ Thus lstat() is used initially; if a symlink is discovered, the loop is
+ repeated such that stat() is used, to look at the end file. */
+
+ for (i = 0; i < ob->lock_retries; i++)
+ {
+ int sleep_before_retry = TRUE;
+ file_opened = FALSE;
+
+ if ((use_lstat ? Ulstat(filename, &statbuf) : Ustat(filename, &statbuf)) != 0)
+ {
+ /* Let's hope that failure to stat (other than non-existence) is a
+ rare event. */
+
+ if (errno != ENOENT)
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("attempting to stat mailbox %s",
+ filename);
+ goto RETURN;
+ }
+
+ /* File does not exist. If it is required to pre-exist this state is an
+ error. */
+
+ if (ob->file_must_exist)
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("mailbox %s does not exist, "
+ "but file_must_exist is set", filename);
+ goto RETURN;
+ }
+
+ /* If not permitted to create this file because it isn't in or below
+ the home directory, generate an error. */
+
+ if (!allow_creation_here)
+ {
+ addr->basic_errno = ERRNO_BADCREATE;
+ addr->message = string_sprintf("mailbox %s does not exist, "
+ "but creation outside the home directory is not permitted",
+ filename);
+ goto RETURN;
+ }
+
+ /* Attempt to create and open the file. If open fails because of
+ pre-existence, go round the loop again. For any other error, defer the
+ address, except for an alias or forward generated file name with EPERM,
+ ENOENT, or EACCES, as those are most likely to be user errors rather
+ than Exim config errors. When a symbolic link is permitted and points
+ to a non-existent file, we get here with use_lstat = FALSE. In this case
+ we mustn't use O_EXCL, since it doesn't work. The file is opened RDRW for
+ consistency and because MBX locking requires it in order to be able to
+ get a shared lock. */
+
+ fd = Uopen(filename, O_RDWR | O_APPEND | O_CREAT |
+ (use_lstat ? O_EXCL : 0), mode);
+ if (fd < 0)
+ {
+ if (errno == EEXIST) continue;
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while creating mailbox %s",
+ filename);
+ if (testflag(addr, af_file) &&
+ (errno == EPERM || errno == ENOENT || errno == EACCES))
+ addr->transport_return = FAIL;
+ goto RETURN;
+ }
+
+ /* We have successfully created and opened the file. Ensure that the group
+ and the mode are correct. */
+
+ if (exim_chown(filename, uid, gid) || Uchmod(filename, mode))
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while setting perms on mailbox %s",
+ filename);
+ addr->transport_return = FAIL;
+ goto RETURN;
+ }
+ }
+
+
+ /* The file already exists. Test its type, ownership, and permissions, and
+ save the inode for checking later. If symlinks are permitted (not the
+ default or recommended state) it may be a symlink that already exists.
+ Check its ownership and then look for the file at the end of the link(s).
+ This at least prevents one user creating a symlink for another user in
+ a sticky directory. */
+
+ else
+ {
+ int oldmode = (int)statbuf.st_mode;
+ ino_t inode = statbuf.st_ino;
+ BOOL islink = (oldmode & S_IFMT) == S_IFLNK;
+
+ isfifo = FALSE; /* In case things are changing */
+
+ /* Check owner if required - the default. */
+
+ if (ob->check_owner && statbuf.st_uid != uid)
+ {
+ addr->basic_errno = ERRNO_BADUGID;
+ addr->message = string_sprintf("mailbox %s%s has wrong uid "
+ "(%ld != %ld)", filename,
+ islink ? " (symlink)" : "",
+ (long int)(statbuf.st_uid), (long int)uid);
+ goto RETURN;
+ }
+
+ /* Group is checked only if check_group is set. */
+
+ if (ob->check_group && statbuf.st_gid != gid)
+ {
+ addr->basic_errno = ERRNO_BADUGID;
+ addr->message = string_sprintf("mailbox %s%s has wrong gid (%d != %d)",
+ filename, islink ? " (symlink)" : "", statbuf.st_gid, gid);
+ goto RETURN;
+ }
+
+ /* Just in case this is a sticky-bit mail directory, we don't want
+ users to be able to create hard links to other users' files. */
+
+ if (statbuf.st_nlink != 1)
+ {
+ addr->basic_errno = ERRNO_NOTREGULAR;
+ addr->message = string_sprintf("mailbox %s%s has too many links (%lu)",
+ filename, islink ? " (symlink)" : "", (unsigned long)statbuf.st_nlink);
+ goto RETURN;
+
+ }
+
+ /* If symlinks are permitted (not recommended), the lstat() above will
+ have found the symlink. Its ownership has just been checked; go round
+ the loop again, using stat() instead of lstat(). That will never yield a
+ mode of S_IFLNK. */
+
+ if (islink && ob->allow_symlink)
+ {
+ use_lstat = FALSE;
+ i--; /* Don't count this time round */
+ continue;
+ }
+
+ /* An actual file exists. Check that it is a regular file, or FIFO
+ if permitted. */
+
+ if (ob->allow_fifo && (oldmode & S_IFMT) == S_IFIFO) isfifo = TRUE;
+
+ else if ((oldmode & S_IFMT) != S_IFREG)
+ {
+ addr->basic_errno = ERRNO_NOTREGULAR;
+ addr->message = string_sprintf("mailbox %s is not a regular file%s",
+ filename, ob->allow_fifo ? " or named pipe" : "");
+ goto RETURN;
+ }
+
+ /* If the mode is not what it would be for a newly created file, change
+ the permissions if the mode is supplied for the address. Otherwise,
+ reduce but do not extend the permissions. If the newly created
+ permissions are greater than the existing permissions, don't change
+ things when the mode is not from the address. */
+
+ if ((oldmode &= 07777) != mode)
+ {
+ int diffs = oldmode ^ mode;
+ if (addr->mode > 0 || (diffs & oldmode) == diffs)
+ {
+ DEBUG(D_transport) debug_printf("chmod %o %s\n", mode, filename);
+ if (Uchmod(filename, mode) < 0)
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("attempting to chmod mailbox %s",
+ filename);
+ goto RETURN;
+ }
+ oldmode = mode;
+ }
+
+ /* Mode not from address, and newly-created permissions are greater
+ than existing permissions. Default is to complain, but it can be
+ configured to go ahead and try to deliver anyway if that's what
+ the administration wants. */
+
+ else if (ob->mode_fail_narrower)
+ {
+ addr->basic_errno = ERRNO_BADMODE;
+ addr->message = string_sprintf("mailbox %s has the wrong mode %o "
+ "(%o expected)", filename, oldmode, mode);
+ goto RETURN;
+ }
+ }
+
+ /* We are happy with the existing file. Open it, and then do further
+ tests to ensure that it is the same file that we were just looking at.
+ If the file does not now exist, restart this loop, going back to using
+ lstat again. For an NFS error, just defer; other opening errors are
+ more serious. The file is opened RDWR so that its format can be checked,
+ and also MBX locking requires the use of a shared (read) lock. However,
+ a FIFO is opened WRONLY + NDELAY so that it fails if there is no process
+ reading the pipe. */
+
+ fd = Uopen(filename, isfifo ? (O_WRONLY|O_NDELAY) : (O_RDWR|O_APPEND),
+ mode);
+ if (fd < 0)
+ {
+ if (errno == ENOENT)
+ {
+ use_lstat = TRUE;
+ continue;
+ }
+ addr->basic_errno = errno;
+ if (isfifo)
+ addr->message = string_sprintf("while opening named pipe %s "
+ "(could mean no process is reading it)", filename);
+ else if (errno != EWOULDBLOCK)
+ addr->message = string_sprintf("while opening mailbox %s", filename);
+ goto RETURN;
+ }
+
+ /* This fstat really shouldn't fail, as we have an open file! There's a
+ dilemma here. We use fstat in order to be sure we are peering at the file
+ we have got open. However, that won't tell us if the file was reached
+ via a symbolic link. We checked this above, but there is a race exposure
+ if the link was created between the previous lstat and the open. However,
+ it would have to be created with the same inode in order to pass the
+ check below. If ob->allow_symlink is set, causing the use of stat rather
+ than lstat above, symbolic links may be there anyway, and the checking is
+ weaker. */
+
+ if (fstat(fd, &statbuf) < 0)
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("attempting to stat open mailbox %s",
+ filename);
+ goto RETURN;
+ }
+
+ /* Check the inode; this is isn't a perfect check, but gives some
+ confidence. */
+
+ if (inode != statbuf.st_ino)
+ {
+ addr->basic_errno = ERRNO_INODECHANGED;
+ addr->message = string_sprintf("opened mailbox %s inode number changed "
+ "from " INO_T_FMT " to " INO_T_FMT, filename, inode, statbuf.st_ino);
+ addr->special_action = SPECIAL_FREEZE;
+ goto RETURN;
+ }
+
+ /* Check it's still a regular file or FIFO, and the uid, gid, and
+ permissions have not changed. */
+
+ if ((!isfifo && (statbuf.st_mode & S_IFMT) != S_IFREG) ||
+ (isfifo && (statbuf.st_mode & S_IFMT) != S_IFIFO))
+ {
+ addr->basic_errno = ERRNO_NOTREGULAR;
+ addr->message =
+ string_sprintf("opened mailbox %s is no longer a %s", filename,
+ isfifo ? "named pipe" : "regular file");
+ addr->special_action = SPECIAL_FREEZE;
+ goto RETURN;
+ }
+
+ if ((ob->check_owner && statbuf.st_uid != uid) ||
+ (ob->check_group && statbuf.st_gid != gid))
+ {
+ addr->basic_errno = ERRNO_BADUGID;
+ addr->message =
+ string_sprintf("opened mailbox %s has wrong uid or gid", filename);
+ addr->special_action = SPECIAL_FREEZE;
+ goto RETURN;
+ }
+
+ if ((statbuf.st_mode & 07777) != oldmode)
+ {
+ addr->basic_errno = ERRNO_BADMODE;
+ addr->message = string_sprintf("opened mailbox %s has wrong mode %o "
+ "(%o expected)", filename, statbuf.st_mode & 07777, mode);
+ addr->special_action = SPECIAL_FREEZE;
+ goto RETURN;
+ }
+
+ /* If file_format is set, check that the format of the file has not
+ changed. Error data is set by the testing function. */
+
+ if (ob->file_format && check_file_format(fd, tblock, addr) != tblock)
+ {
+ addr->message = US"open mailbox has changed format";
+ goto RETURN;
+ }
+
+ /* The file is OK. Carry on to do the locking. */
+ }
+
+ /* We now have an open file, and must lock it using fcntl(), flock() or MBX
+ locking rules if configured to do so. If a lock file is also required, it
+ was created above and hd was left >= 0. At least one form of locking is
+ required by the initialization function. If locking fails here, close the
+ file and go round the loop all over again, after waiting for a bit, unless
+ blocking locking was used. */
+
+ file_opened = TRUE;
+ if ((ob->lock_fcntl_timeout > 0) || (ob->lock_flock_timeout > 0))
+ sleep_before_retry = FALSE;
+
+ /* Simple fcntl() and/or flock() locking */
+
+ if (!ob->use_mbx_lock && (ob->use_fcntl || ob->use_flock))
+ {
+ if (apply_lock(fd, F_WRLCK, ob->use_fcntl, ob->lock_fcntl_timeout,
+ ob->use_flock, ob->lock_flock_timeout) >= 0) break;
+ }
+
+ /* MBX locking rules */
+
+ #ifdef SUPPORT_MBX
+ else if (ob->use_mbx_lock)
+ {
+ int mbx_tmp_oflags;
+ struct stat lstatbuf, statbuf2;
+ if (apply_lock(fd, F_RDLCK, ob->use_fcntl, ob->lock_fcntl_timeout,
+ ob->use_flock, ob->lock_flock_timeout) >= 0 &&
+ fstat(fd, &statbuf) >= 0)
+ {
+ sprintf(CS mbx_lockname, "/tmp/.%lx.%lx", (long)statbuf.st_dev,
+ (long)statbuf.st_ino);
+
+ /*
+ * 2010-05-29: SECURITY
+ * Dan Rosenberg reported the presence of a race-condition in the
+ * original code here. Beware that many systems still allow symlinks
+ * to be followed in /tmp so an attacker can create a symlink pointing
+ * elsewhere between a stat and an open, which we should avoid
+ * following.
+ *
+ * It's unfortunate that we can't just use all the heavily debugged
+ * locking from above.
+ *
+ * Also: remember to mirror changes into exim_lock.c */
+
+ /* first leave the old pre-check in place, it provides better
+ * diagnostics for common cases */
+ if (Ulstat(mbx_lockname, &statbuf) >= 0)
+ {
+ if ((statbuf.st_mode & S_IFMT) == S_IFLNK)
+ {
+ addr->basic_errno = ERRNO_LOCKFAILED;
+ addr->message = string_sprintf("symbolic link on MBX lock file %s",
+ mbx_lockname);
+ goto RETURN;
+ }
+ if (statbuf.st_nlink > 1)
+ {
+ addr->basic_errno = ERRNO_LOCKFAILED;
+ addr->message = string_sprintf("hard link to MBX lock file %s",
+ mbx_lockname);
+ goto RETURN;
+ }
+ }
+
+ /* If we could just declare "we must be the ones who create this
+ * file" then a hitching post in a subdir would work, since a
+ * subdir directly in /tmp/ which we create wouldn't follow links
+ * but this isn't our locking logic, so we can't safely change the
+ * file existence rules. */
+
+ /* On systems which support O_NOFOLLOW, it's the easiest and most
+ * obviously correct security fix */
+ mbx_tmp_oflags = O_RDWR | O_CREAT;
+#ifdef O_NOFOLLOW
+ mbx_tmp_oflags |= O_NOFOLLOW;
+#endif
+ mbx_lockfd = Uopen(mbx_lockname, mbx_tmp_oflags, ob->lockfile_mode);
+ if (mbx_lockfd < 0)
+ {
+ addr->basic_errno = ERRNO_LOCKFAILED;
+ addr->message = string_sprintf("failed to open MBX lock file %s :%s",
+ mbx_lockname, strerror(errno));
+ goto RETURN;
+ }
+
+ if (Ulstat(mbx_lockname, &lstatbuf) < 0)
+ {
+ addr->basic_errno = ERRNO_LOCKFAILED;
+ addr->message = string_sprintf("attempting to lstat open MBX "
+ "lock file %s: %s", mbx_lockname, strerror(errno));
+ goto RETURN;
+ }
+ if (fstat(mbx_lockfd, &statbuf2) < 0)
+ {
+ addr->basic_errno = ERRNO_LOCKFAILED;
+ addr->message = string_sprintf("attempting to stat fd of open MBX "
+ "lock file %s: %s", mbx_lockname, strerror(errno));
+ goto RETURN;
+ }
+
+ /*
+ * At this point:
+ * statbuf: if exists, is file which existed prior to opening the
+ * lockfile, might have been replaced since then
+ * statbuf2: result of stat'ing the open fd, is what was actually
+ * opened
+ * lstatbuf: result of lstat'ing the filename immediately after
+ * the open but there's a race condition again between
+ * those two steps: before open, symlink to foo, after
+ * open but before lstat have one of:
+ * * was no symlink, so is the opened file
+ * (we created it, no messing possible after that point)
+ * * hardlink to foo
+ * * symlink elsewhere
+ * * hardlink elsewhere
+ * * new file/other
+ * Don't want to compare to device of /tmp because some modern systems
+ * have regressed to having /tmp be the safe actual filesystem as
+ * valuable data, so is mostly worthless, unless we assume that *only*
+ * Linux systems do this and that all Linux has O_NOFOLLOW. Something
+ * for further consideration.
+ * No point in doing a readlink on the lockfile as that will always be
+ * at a different point in time from when we open it, so tells us
+ * nothing; attempts to clean up and delete after ourselves would risk
+ * deleting a *third* filename.
+ */
+ if ((statbuf2.st_nlink > 1) ||
+ (lstatbuf.st_nlink > 1) ||
+ (!S_ISREG(lstatbuf.st_mode)) ||
+ (lstatbuf.st_dev != statbuf2.st_dev) ||
+ (lstatbuf.st_ino != statbuf2.st_ino))
+ {
+ addr->basic_errno = ERRNO_LOCKFAILED;
+ addr->message = string_sprintf("RACE CONDITION detected: "
+ "mismatch post-initial-checks between \"%s\" and opened "
+ "fd lead us to abort!", mbx_lockname);
+ goto RETURN;
+ }
+
+ (void)Uchmod(mbx_lockname, ob->lockfile_mode);
+
+ if (apply_lock(mbx_lockfd, F_WRLCK, ob->use_fcntl,
+ ob->lock_fcntl_timeout, ob->use_flock, ob->lock_flock_timeout) >= 0)
+ {
+ struct stat ostatbuf;
+
+ /* This tests for a specific race condition. Ensure that we still
+ have the same file. */
+
+ if (Ulstat(mbx_lockname, &statbuf) == 0 &&
+ fstat(mbx_lockfd, &ostatbuf) == 0 &&
+ statbuf.st_dev == ostatbuf.st_dev &&
+ statbuf.st_ino == ostatbuf.st_ino)
+ break;
+ DEBUG(D_transport) debug_printf("MBX lockfile %s changed "
+ "between creation and locking\n", mbx_lockname);
+ }
+
+ DEBUG(D_transport) debug_printf("failed to lock %s: %s\n", mbx_lockname,
+ strerror(errno));
+ (void)close(mbx_lockfd);
+ mbx_lockfd = -1;
+ }
+ else
+ {
+ DEBUG(D_transport) debug_printf("failed to fstat or get read lock on %s: %s\n",
+ filename, strerror(errno));
+ }
+ }
+ #endif /* SUPPORT_MBX */
+
+ else break; /* No on-file locking required; break the open/lock loop */
+
+ DEBUG(D_transport)
+ debug_printf("fcntl(), flock(), or MBX locking failed - retrying\n");
+
+ (void)close(fd);
+ fd = -1;
+ use_lstat = TRUE; /* Reset to use lstat first */
+
+
+ /* If a blocking call timed out, break the retry loop if the total time
+ so far is not less than than retries * interval. Use the larger of the
+ flock() and fcntl() timeouts. */
+
+ if (sigalrm_seen &&
+ (i+1) * ((ob->lock_fcntl_timeout > ob->lock_flock_timeout)?
+ ob->lock_fcntl_timeout : ob->lock_flock_timeout) >=
+ ob->lock_retries * ob->lock_interval)
+ i = ob->lock_retries;
+
+ /* Wait a bit before retrying, except when it was a blocked fcntl() or
+ flock() that caused the problem. */
+
+ if (i < ob->lock_retries && sleep_before_retry) sleep(ob->lock_interval);
+ }
+
+ /* Test for exceeding the maximum number of tries. Either the file remains
+ locked, or, if we haven't got it open, something is terribly wrong... */
+
+ if (i >= ob->lock_retries)
+ {
+ if (!file_opened)
+ {
+ addr->basic_errno = ERRNO_EXISTRACE;
+ addr->message = string_sprintf("mailbox %s: existence unclear", filename);
+ addr->special_action = SPECIAL_FREEZE;
+ }
+ else
+ {
+ addr->basic_errno = ERRNO_LOCKFAILED;
+ addr->message = string_sprintf("failed to lock mailbox %s (fcntl/flock)",
+ filename);
+ }
+ goto RETURN;
+ }
+
+ DEBUG(D_transport) debug_printf("mailbox %s is locked\n", filename);
+
+ /* Save access time (for subsequent restoration), modification time (for
+ restoration if updating fails), size of file (for comsat and for re-setting if
+ delivery fails in the middle - e.g. for quota exceeded). */
+
+ if (fstat(fd, &statbuf) < 0)
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while fstatting opened mailbox %s",
+ filename);
+ goto RETURN;
+ }
+
+ times.actime = statbuf.st_atime;
+ times.modtime = statbuf.st_mtime;
+ saved_size = statbuf.st_size;
+ if (mailbox_size < 0) mailbox_size = saved_size;
+ mailbox_filecount = 0; /* Not actually relevant for single-file mailbox */
+ }
+
+/* Prepare for writing to a new file (as opposed to appending to an old one).
+There are several different formats, but there is preliminary stuff concerned
+with quotas that applies to all of them. Finding the current size by directory
+scanning is expensive; for maildirs some fudges have been invented:
+
+ (1) A regex can be used to extract a file size from its name;
+ (2) If maildir_use_size is set, a maildirsize file is used to cache the
+ mailbox size.
+*/
+
+else
+ {
+ uschar *check_path; /* Default quota check path */
+ const pcre2_code * re = NULL; /* Regex for file size from file name */
+
+ if (!check_creation(string_sprintf("%s/any", path),
+ ob->create_file, deliver_dir))
+ {
+ addr->basic_errno = ERRNO_BADCREATE;
+ addr->message = string_sprintf("tried to create file in %s, but "
+ "file creation outside the home directory is not permitted", path);
+ goto RETURN;
+ }
+
+ /* If the create_file option is an absolute path and the file was within
+ it, de-taint. Otherwise check for taint. */
+
+ if (is_tainted(path))
+ if (ob->create_file == create_belowhome)
+ {
+ DEBUG(D_transport) debug_printf("de-tainting path '%s'\n", path);
+ path = string_copy_taint(path, GET_UNTAINTED);
+ }
+ else
+ goto tainted_ret_panic;
+
+ check_path = path;
+
+ #ifdef SUPPORT_MAILDIR
+ /* For a maildir delivery, ensure that all the relevant directories exist,
+ and a maildirfolder file if necessary. */
+
+ if (mbformat == mbf_maildir && !maildir_ensure_directories(path, addr,
+ ob->create_directory, ob->dirmode, ob->maildirfolder_create_regex))
+ return FALSE;
+ #endif /* SUPPORT_MAILDIR */
+
+ /* If we are going to do a quota check, of if maildir_use_size_file is set
+ for a maildir delivery, compile the regular expression if there is one. We
+ may also need to adjust the path that is used. We need to do this for
+ maildir_use_size_file even if the quota is unset, because we still want to
+ create the file. When maildir support is not compiled,
+ ob->maildir_use_size_file is always FALSE. */
+
+ if (ob->quota_value > 0 || THRESHOLD_CHECK || ob->maildir_use_size_file)
+ {
+ PCRE2_SIZE offset;
+ int err;
+
+ /* Compile the regex if there is one. */
+
+ if (ob->quota_size_regex)
+ {
+ if (!(re = pcre2_compile((PCRE2_SPTR)ob->quota_size_regex,
+ PCRE2_ZERO_TERMINATED, PCRE_COPT, &err, &offset, pcre_cmp_ctx)))
+ {
+ uschar errbuf[128];
+ pcre2_get_error_message(err, errbuf, sizeof(errbuf));
+ addr->message = string_sprintf("appendfile: regular expression "
+ "error: %s at offset %ld while compiling %s", errbuf, (long)offset,
+ ob->quota_size_regex);
+ return FALSE;
+ }
+ DEBUG(D_transport) debug_printf("using regex for file sizes: %s\n",
+ ob->quota_size_regex);
+ }
+
+ /* Use an explicitly configured directory if set */
+
+ if (ob->quota_directory)
+ {
+ if (!(check_path = expand_string(ob->quota_directory)))
+ {
+ addr->message = string_sprintf("Expansion of \"%s\" (quota_directory "
+ "name for %s transport) failed: %s", ob->quota_directory,
+ tblock->name, expand_string_message);
+ goto ret_panic;
+ }
+
+ if (check_path[0] != '/')
+ {
+ addr->message = string_sprintf("appendfile: quota_directory name "
+ "\"%s\" is not absolute", check_path);
+ addr->basic_errno = ERRNO_NOTABSOLUTE;
+ return FALSE;
+ }
+ }
+
+ #ifdef SUPPORT_MAILDIR
+ /* Otherwise, if we are handling a maildir delivery, and the directory
+ contains a file called maildirfolder, this is a maildir++ feature telling
+ us that this is a sub-directory of the real inbox. We should therefore do
+ the quota check on the parent directory. Beware of the special case when
+ the directory name itself ends in a slash. */
+
+ else if (mbformat == mbf_maildir)
+ {
+ struct stat statbuf;
+ if (Ustat(string_sprintf("%s/maildirfolder", path), &statbuf) >= 0)
+ {
+ uschar *new_check_path = string_copy(check_path);
+ uschar *slash = Ustrrchr(new_check_path, '/');
+ if (slash)
+ {
+ if (!slash[1])
+ {
+ *slash = 0;
+ slash = Ustrrchr(new_check_path, '/');
+ }
+ if (slash)
+ {
+ *slash = 0;
+ check_path = new_check_path;
+ DEBUG(D_transport) debug_printf("maildirfolder file exists: "
+ "quota check directory changed to %s\n", check_path);
+ }
+ }
+ }
+ }
+ #endif /* SUPPORT_MAILDIR */
+ }
+
+ /* If we are using maildirsize files, we need to ensure that such a file
+ exists and, if necessary, recalculate its contents. As a byproduct of this,
+ we obtain the current size of the maildir. If no quota is to be enforced
+ (ob->quota_value == 0), we still need the size if a threshold check will
+ happen later.
+
+ Another regular expression is used to determine which directories inside the
+ maildir are going to be counted. */
+
+ #ifdef SUPPORT_MAILDIR
+ if (ob->maildir_use_size_file)
+ {
+ const pcre2_code * dir_regex = NULL;
+ PCRE2_SIZE offset;
+ int err;
+
+ if (ob->maildir_dir_regex)
+ {
+ int check_path_len = Ustrlen(check_path);
+
+ if (!(dir_regex = pcre2_compile((PCRE2_SPTR)ob->maildir_dir_regex,
+ PCRE2_ZERO_TERMINATED, PCRE_COPT, &err, &offset, pcre_cmp_ctx)))
+ {
+ uschar errbuf[128];
+ pcre2_get_error_message(err, errbuf, sizeof(errbuf));
+ addr->message = string_sprintf("appendfile: regular expression "
+ "error: %s at offset %ld while compiling %s", errbuf, (long)offset,
+ ob->maildir_dir_regex);
+ return FALSE;
+ }
+
+ DEBUG(D_transport)
+ debug_printf("using regex for maildir directory selection: %s\n",
+ ob->maildir_dir_regex);
+
+ /* Check to see if we are delivering into an ignored directory, that is,
+ if the delivery path starts with the quota check path, and the rest
+ of the deliver path matches the regex; if so, set a flag to disable quota
+ checking and maildirsize updating. */
+
+ if (Ustrncmp(path, check_path, check_path_len) == 0)
+ {
+ uschar *s = path + check_path_len;
+ while (*s == '/') s++;
+ s = *s ? string_sprintf("%s/new", s) : US"new";
+ if (!regex_match(dir_regex, s, -1, NULL))
+ {
+ disable_quota = TRUE;
+ DEBUG(D_transport) debug_printf("delivery directory does not match "
+ "maildir_quota_directory_regex: disabling quota\n");
+ }
+ }
+ }
+
+ /* Quota enforcement; create and check the file. There is some discussion
+ about whether this should happen if the quota is unset. At present, Exim
+ always creates the file. If we ever want to change this, uncomment
+ appropriate lines below, possibly doing a check on some option. */
+
+/* if (???? || ob->quota_value > 0) */
+
+ if (!disable_quota)
+ {
+ off_t size;
+ int filecount;
+
+ if ((maildirsize_fd = maildir_ensure_sizefile(check_path, ob, re, dir_regex,
+ &size, &filecount)) == -1)
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while opening or reading "
+ "%s/maildirsize", check_path);
+ return FALSE;
+ }
+ /* can also return -2, which means that the file was removed because of
+ raciness; but in this case, the size & filecount will still have been
+ updated. */
+
+ if (mailbox_size < 0) mailbox_size = size;
+ if (mailbox_filecount < 0) mailbox_filecount = filecount;
+ }
+
+ /* No quota enforcement; ensure file does *not* exist; calculate size if
+ needed. */
+
+/* else
+ * {
+ * time_t old_latest;
+ * (void)unlink(CS string_sprintf("%s/maildirsize", check_path));
+ * if (THRESHOLD_CHECK)
+ * mailbox_size = maildir_compute_size(check_path, &mailbox_filecount, &old_latest,
+ * re, dir_regex, FALSE);
+ * }
+*/
+
+ }
+ #endif /* SUPPORT_MAILDIR */
+
+ /* Otherwise if we are going to do a quota check later on, and the mailbox
+ size is not set, find the current size of the mailbox. Ditto for the file
+ count. Note that ob->quota_filecount_value cannot be set without
+ ob->quota_value being set. */
+
+ if ( !disable_quota
+ && (ob->quota_value > 0 || THRESHOLD_CHECK)
+ && ( mailbox_size < 0
+ || mailbox_filecount < 0 && ob->quota_filecount_value > 0
+ ) )
+ {
+ off_t size;
+ int filecount = 0;
+ DEBUG(D_transport)
+ debug_printf("quota checks on directory %s\n", check_path);
+ size = check_dir_size(check_path, &filecount, re);
+ if (mailbox_size < 0) mailbox_size = size;
+ if (mailbox_filecount < 0) mailbox_filecount = filecount;
+ }
+
+ /* Handle the case of creating a unique file in a given directory (not in
+ maildir or mailstore format - this is how smail did it). A temporary name is
+ used to create the file. Later, when it is written, the name is changed to a
+ unique one. There is no need to lock the file. An attempt is made to create
+ the directory if it does not exist. */
+
+ if (mbformat == mbf_smail)
+ {
+ DEBUG(D_transport)
+ debug_printf("delivering to new file in %s\n", path);
+ filename = dataname =
+ string_sprintf("%s/temp.%d.%s", path, (int)getpid(), primary_hostname);
+ fd = Uopen(filename, O_WRONLY|O_CREAT, mode);
+ if (fd < 0 && /* failed to open, and */
+ (errno != ENOENT || /* either not non-exist */
+ !ob->create_directory || /* or not allowed to make */
+ !directory_make(NULL, path, ob->dirmode, FALSE) || /* or failed to create dir */
+ (fd = Uopen(filename, O_WRONLY|O_CREAT|O_EXCL, mode)) < 0)) /* or then failed to open */
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while creating file %s", filename);
+ return FALSE;
+ }
+ }
+
+ #ifdef SUPPORT_MAILDIR
+
+ /* Handle the case of a unique file in maildir format. The file is written to
+ the tmp subdirectory, with a prescribed form of name. */
+
+ else if (mbformat == mbf_maildir)
+ {
+ DEBUG(D_transport)
+ debug_printf("delivering in maildir format in %s\n", path);
+
+ nametag = ob->maildir_tag;
+
+ /* Check that nametag expands successfully; a hard failure causes a panic
+ return. The actual expansion for use happens again later, when
+ $message_size is accurately known. */
+
+ if (nametag && !expand_string(nametag) && !f.expand_string_forcedfail)
+ {
+ addr->message = string_sprintf("Expansion of \"%s\" (maildir_tag "
+ "for %s transport) failed: %s", nametag, tblock->name,
+ expand_string_message);
+ goto ret_panic;
+ }
+
+ /* We ensured the existence of all the relevant directories above. Attempt
+ to open the temporary file a limited number of times. I think this rather
+ scary-looking for statement is actually OK. If open succeeds, the loop is
+ broken; if not, there is a test on the value of i. Get the time again
+ afresh each time round the loop. Its value goes into a variable that is
+ checked at the end, to make sure we don't release this process until the
+ clock has ticked. */
+
+ for (int i = 1;; i++)
+ {
+ uschar *basename;
+
+ (void)gettimeofday(&msg_tv, NULL);
+ basename = string_sprintf(TIME_T_FMT ".M%luP" PID_T_FMT ".%s",
+ msg_tv.tv_sec, msg_tv.tv_usec, getpid(), primary_hostname);
+
+ filename = dataname = string_sprintf("tmp/%s", basename);
+ newname = string_sprintf("new/%s", basename);
+
+ if (Ustat(filename, &statbuf) == 0)
+ errno = EEXIST;
+ else if (errno == ENOENT)
+ {
+ if ((fd = Uopen(filename, O_WRONLY | O_CREAT | O_EXCL, mode)) >= 0)
+ break;
+ DEBUG (D_transport) debug_printf ("open failed for %s: %s\n",
+ filename, strerror(errno));
+ }
+
+ /* Too many retries - give up */
+
+ if (i >= ob->maildir_retries)
+ {
+ addr->message = string_sprintf ("failed to open %s (%d tr%s)",
+ filename, i, (i == 1) ? "y" : "ies");
+ addr->basic_errno = errno;
+ if (errno == errno_quota || errno == ENOSPC)
+ addr->user_message = US"mailbox is full";
+ return FALSE;
+ }
+
+ /* Open or stat failed but we haven't tried too many times yet. */
+
+ sleep(2);
+ }
+
+ /* Note that we have to ensure the clock has ticked before leaving */
+
+ wait_for_tick = TRUE;
+
+ /* Why are these here? Put in because they are present in the non-maildir
+ directory case above. */
+
+ if (exim_chown(filename, uid, gid) || Uchmod(filename, mode))
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while setting perms on maildir %s",
+ filename);
+ return FALSE;
+ }
+ }
+
+ #endif /* SUPPORT_MAILDIR */
+
+ #ifdef SUPPORT_MAILSTORE
+
+ /* Handle the case of a unique file in mailstore format. First write the
+ envelope to a temporary file, then open the main file. The unique base name
+ for the files consists of the message id plus the pid of this delivery
+ process. */
+
+ else
+ {
+ FILE *env_file;
+ mailstore_basename = string_sprintf("%s/%s-%s", path, message_id,
+ string_base62((long int)getpid()));
+
+ DEBUG(D_transport)
+ debug_printf("delivering in mailstore format in %s\n", path);
+
+ filename = string_sprintf("%s.tmp", mailstore_basename);
+ newname = string_sprintf("%s.env", mailstore_basename);
+ dataname = string_sprintf("%s.msg", mailstore_basename);
+
+ fd = Uopen(filename, O_WRONLY|O_CREAT|O_EXCL, mode);
+ if ( fd < 0 /* failed to open, and */
+ && ( errno != ENOENT /* either not non-exist */
+ || !ob->create_directory /* or not allowed to make */
+ || !directory_make(NULL, path, ob->dirmode, FALSE) /* or failed to create dir */
+ || (fd = Uopen(filename, O_WRONLY|O_CREAT|O_EXCL, mode)) < 0 /* or then failed to open */
+ ) )
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while creating file %s", filename);
+ return FALSE;
+ }
+
+ /* Why are these here? Put in because they are present in the non-maildir
+ directory case above. */
+
+ if (exim_chown(filename, uid, gid) || Uchmod(filename, mode))
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while setting perms on file %s",
+ filename);
+ return FALSE;
+ }
+
+ /* Built a C stream from the open file descriptor. */
+
+ if (!(env_file = fdopen(fd, "wb")))
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("fdopen of %s ("
+ "for %s transport) failed", filename, tblock->name);
+ (void)close(fd);
+ Uunlink(filename);
+ goto ret_panic;
+ }
+
+ /* Write the envelope file, then close it. */
+
+ if (ob->mailstore_prefix)
+ {
+ uschar *s = expand_string(ob->mailstore_prefix);
+ if (!s)
+ {
+ if (!f.expand_string_forcedfail)
+ {
+ addr->message = string_sprintf("Expansion of \"%s\" (mailstore "
+ "prefix for %s transport) failed: %s", ob->mailstore_prefix,
+ tblock->name, expand_string_message);
+ (void)fclose(env_file);
+ Uunlink(filename);
+ goto ret_panic;
+ }
+ }
+ else
+ {
+ int n = Ustrlen(s);
+ fprintf(env_file, "%s", CS s);
+ if (n == 0 || s[n-1] != '\n') fprintf(env_file, "\n");
+ }
+ }
+
+ fprintf(env_file, "%s\n", sender_address);
+
+ for (address_item * taddr = addr; taddr; taddr = taddr->next)
+ fprintf(env_file, "%s@%s\n", taddr->local_part, taddr->domain);
+
+ if (ob->mailstore_suffix)
+ {
+ uschar *s = expand_string(ob->mailstore_suffix);
+ if (!s)
+ {
+ if (!f.expand_string_forcedfail)
+ {
+ addr->message = string_sprintf("Expansion of \"%s\" (mailstore "
+ "suffix for %s transport) failed: %s", ob->mailstore_suffix,
+ tblock->name, expand_string_message);
+ (void)fclose(env_file);
+ Uunlink(filename);
+ goto ret_panic;
+ }
+ }
+ else
+ {
+ int n = Ustrlen(s);
+ fprintf(env_file, "%s", CS s);
+ if (n == 0 || s[n-1] != '\n') fprintf(env_file, "\n");
+ }
+ }
+
+ if (fclose(env_file) != 0)
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while closing %s", filename);
+ Uunlink(filename);
+ return FALSE;
+ }
+
+ DEBUG(D_transport) debug_printf("Envelope file %s written\n", filename);
+
+ /* Now open the data file, and ensure that it has the correct ownership and
+ mode. */
+
+ if ((fd = Uopen(dataname, O_WRONLY|O_CREAT|O_EXCL, mode)) < 0)
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while creating file %s", dataname);
+ Uunlink(filename);
+ return FALSE;
+ }
+ if (exim_chown(dataname, uid, gid) || Uchmod(dataname, mode))
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while setting perms on file %s",
+ dataname);
+ return FALSE;
+ }
+ }
+
+ #endif /* SUPPORT_MAILSTORE */
+
+
+ /* In all cases of writing to a new file, ensure that the file which is
+ going to be renamed has the correct ownership and mode. */
+
+ if (exim_chown(filename, uid, gid) || Uchmod(filename, mode))
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while setting perms on file %s",
+ filename);
+ return FALSE;
+ }
+ }
+
+
+/* At last we can write the message to the file, preceded by any configured
+prefix line, and followed by any configured suffix line. If there are any
+writing errors, we must defer. */
+
+DEBUG(D_transport) debug_printf("writing to file %s\n", dataname);
+
+yield = OK;
+errno = 0;
+
+/* If there is a local quota setting, check that we are not going to exceed it
+with this message if quota_is_inclusive is set; if it is not set, the check
+is for the mailbox already being over quota (i.e. the current message is not
+included in the check). */
+
+if (!disable_quota && ob->quota_value > 0)
+ {
+ DEBUG(D_transport)
+ {
+ debug_printf("Exim quota = " OFF_T_FMT " old size = " OFF_T_FMT
+ " this message = %d (%sincluded)\n",
+ ob->quota_value, mailbox_size, message_size,
+ ob->quota_is_inclusive ? "" : "not ");
+ debug_printf(" file count quota = %d count = %d\n",
+ ob->quota_filecount_value, mailbox_filecount);
+ }
+
+ if (mailbox_size + (ob->quota_is_inclusive ? message_size:0) > ob->quota_value)
+ if (!ob->quota_no_check)
+ {
+ DEBUG(D_transport) debug_printf("mailbox quota exceeded\n");
+ yield = DEFER;
+ errno = ERRNO_EXIMQUOTA;
+ }
+ else
+ DEBUG(D_transport) debug_printf("mailbox quota exceeded but ignored\n");
+
+ if (ob->quota_filecount_value > 0
+ && mailbox_filecount + (ob->quota_is_inclusive ? 1:0) >
+ ob->quota_filecount_value)
+ if (!ob->quota_filecount_no_check)
+ {
+ DEBUG(D_transport) debug_printf("mailbox file count quota exceeded\n");
+ yield = DEFER;
+ errno = ERRNO_EXIMQUOTA;
+ filecount_msg = US" filecount";
+ }
+ else DEBUG(D_transport) if (ob->quota_filecount_no_check)
+ debug_printf("mailbox file count quota exceeded but ignored\n");
+
+ }
+
+if (verify_mode)
+ {
+ addr->basic_errno = errno;
+ addr->message = US"Over quota";
+ addr->transport_return = yield;
+ DEBUG(D_transport)
+ debug_printf("appendfile (verify) yields %d with errno=%d more_errno=%d\n",
+ yield, addr->basic_errno, addr->more_errno);
+
+ goto RETURN;
+ }
+
+/* If we are writing in MBX format, what we actually do is to write the message
+to a temporary file, and then copy it to the real file once we know its size.
+This is the most straightforward way of getting the correct length in the
+separator line. So, what we do here is to save the real file descriptor, and
+replace it with one for a temporary file. The temporary file gets unlinked once
+opened, so that it goes away on closure. */
+
+#ifdef SUPPORT_MBX
+if (yield == OK && ob->mbx_format)
+ {
+ if (!(temp_file = tmpfile()))
+ {
+ addr->basic_errno = errno;
+ addr->message = US"while setting up temporary file";
+ yield = DEFER;
+ goto RETURN;
+ }
+ save_fd = fd;
+ fd = fileno(temp_file);
+ DEBUG(D_transport) debug_printf("writing to temporary file\n");
+ }
+#endif /* SUPPORT_MBX */
+
+/* Zero the count of bytes written. It is incremented by the transport_xxx()
+functions. */
+
+transport_count = 0;
+transport_newlines = 0;
+
+/* Write any configured prefix text first */
+
+if (yield == OK && ob->message_prefix && *ob->message_prefix)
+ {
+ uschar *prefix = expand_string(ob->message_prefix);
+ if (!prefix)
+ {
+ errno = ERRNO_EXPANDFAIL;
+ addr->transport_return = PANIC;
+ addr->message = string_sprintf("Expansion of \"%s\" (prefix for %s "
+ "transport) failed", ob->message_prefix, tblock->name);
+ yield = DEFER;
+ }
+ else if (!transport_write_string(fd, "%s", prefix)) yield = DEFER;
+ }
+
+/* If the use_bsmtp option is on, we need to write SMTP prefix information. The
+various different values for batching are handled outside; if there is more
+than one address available here, all must be included. If any address is a
+file, use its parent in the RCPT TO. */
+
+if (yield == OK && ob->use_bsmtp)
+ {
+ transport_count = 0;
+ transport_newlines = 0;
+ if (ob->use_crlf) cr = US"\r";
+ if (!transport_write_string(fd, "MAIL FROM:<%s>%s\n", return_path, cr))
+ yield = DEFER;
+ else
+ {
+ transport_newlines++;
+ for (address_item * a = addr; a; a = a->next)
+ {
+ address_item * b = testflag(a, af_pfr) ? a->parent : a;
+ if (!transport_write_string(fd, "RCPT TO:<%s>%s\n",
+ transport_rcpt_address(b, tblock->rcpt_include_affixes), cr))
+ { yield = DEFER; break; }
+ transport_newlines++;
+ }
+ if (yield == OK && !transport_write_string(fd, "DATA%s\n", cr))
+ yield = DEFER;
+ else
+ transport_newlines++;
+ }
+ }
+
+/* Now the message itself. The options for transport_write_message were set up
+at initialization time. */
+
+if (yield == OK)
+ {
+ transport_ctx tctx = {
+ .u = {.fd=fd},
+ .tblock = tblock,
+ .addr = addr,
+ .check_string = ob->check_string,
+ .escape_string = ob->escape_string,
+ .options = ob->options | topt_not_socket
+ };
+ if (!transport_write_message(&tctx, 0))
+ yield = DEFER;
+ }
+
+/* Now a configured suffix. */
+
+if (yield == OK && ob->message_suffix && *ob->message_suffix)
+ {
+ uschar *suffix = expand_string(ob->message_suffix);
+ if (!suffix)
+ {
+ errno = ERRNO_EXPANDFAIL;
+ addr->transport_return = PANIC;
+ addr->message = string_sprintf("Expansion of \"%s\" (suffix for %s "
+ "transport) failed", ob->message_suffix, tblock->name);
+ yield = DEFER;
+ }
+ else if (!transport_write_string(fd, "%s", suffix)) yield = DEFER;
+ }
+
+/* If batch smtp, write the terminating dot. */
+
+if (yield == OK && ob->use_bsmtp)
+ if (!transport_write_string(fd, ".%s\n", cr)) yield = DEFER;
+ else transport_newlines++;
+
+/* If MBX format is being used, all that writing was to the temporary file.
+However, if there was an earlier failure (Exim quota exceeded, for example),
+the temporary file won't have got opened - and no writing will have been done.
+If writing was OK, we restore the fd, and call a function that copies the
+message in MBX format into the real file. Otherwise use the temporary name in
+any messages. */
+
+#ifdef SUPPORT_MBX
+if (temp_file && ob->mbx_format)
+ {
+ int mbx_save_errno;
+ fd = save_fd;
+
+ if (yield == OK)
+ {
+ transport_count = 0; /* Reset transport count for actual write */
+ /* No need to reset transport_newlines as we're just using a block copy
+ * routine so the number won't be affected */
+ yield = copy_mbx_message(fd, fileno(temp_file), saved_size);
+ }
+ else if (errno >= 0) dataname = US"temporary file";
+
+ /* Preserve errno while closing the temporary file. */
+
+ mbx_save_errno = errno;
+ (void)fclose(temp_file);
+ errno = mbx_save_errno;
+ }
+#endif /* SUPPORT_MBX */
+
+/* Force out the remaining data to check for any errors; some OS don't allow
+fsync() to be called for a FIFO. */
+
+if (yield == OK && !isfifo && EXIMfsync(fd) < 0) yield = DEFER;
+
+/* Update message_size and message_linecount to the accurate count of bytes
+written, including added headers. Note; we subtract 1 from message_linecount as
+this variable doesn't count the new line between the header and the body of the
+message. */
+
+message_size = transport_count;
+message_linecount = transport_newlines - 1;
+
+/* If using a maildir++ quota file, add this message's size to it, and
+close the file descriptor, except when the quota has been disabled because we
+are delivering into an uncounted folder. */
+
+#ifdef SUPPORT_MAILDIR
+if (!disable_quota)
+ {
+ if (yield == OK && maildirsize_fd >= 0)
+ maildir_record_length(maildirsize_fd, message_size);
+ maildir_save_errno = errno; /* Preserve errno while closing the file */
+ if (maildirsize_fd >= 0)
+ (void)close(maildirsize_fd);
+ errno = maildir_save_errno;
+ }
+#endif /* SUPPORT_MAILDIR */
+
+/* If there is a quota warning threshold and we are have crossed it with this
+message, set the SPECIAL_WARN flag in the address, to cause a warning message
+to be sent. */
+
+if (!disable_quota && THRESHOLD_CHECK)
+ {
+ off_t threshold = ob->quota_warn_threshold_value;
+ if (ob->quota_warn_threshold_is_percent)
+ threshold = (off_t)(((double)ob->quota_value * threshold) / 100);
+ DEBUG(D_transport)
+ debug_printf("quota = " OFF_T_FMT
+ " threshold = " OFF_T_FMT
+ " old size = " OFF_T_FMT
+ " message size = %d\n",
+ ob->quota_value, threshold, mailbox_size,
+ message_size);
+ if (mailbox_size <= threshold && mailbox_size + message_size > threshold)
+ addr->special_action = SPECIAL_WARN;
+
+ /******* You might think that the test ought to be this:
+ *
+ * if (ob->quota_value > 0 && threshold > 0 && mailbox_size > 0 &&
+ * mailbox_size <= threshold && mailbox_size + message_size > threshold)
+ *
+ * (indeed, I was sent a patch with that in). However, it is possible to
+ * have a warning threshold without actually imposing a quota, and I have
+ * therefore kept Exim backwards compatible.
+ ********/
+
+ }
+
+/* Handle error while writing the file. Control should come here directly after
+the error, with the reason in errno. In the case of expansion failure in prefix
+or suffix, it will be ERRNO_EXPANDFAIL. */
+
+if (yield != OK)
+ {
+ addr->special_action = SPECIAL_NONE; /* Cancel any quota warning */
+
+ /* Save the error number. If positive, it will ultimately cause a strerror()
+ call to generate some text. */
+
+ addr->basic_errno = errno;
+
+ /* For system or Exim quota excession, or disk full, set more_errno to the
+ time since the file was last read. If delivery was into a directory, the
+ time since last read logic is not relevant, in general. However, for maildir
+ deliveries we can approximate it by looking at the last modified time of the
+ "new" subdirectory. Since Exim won't be adding new messages, a change to the
+ "new" subdirectory implies that an MUA has moved a message from there to the
+ "cur" directory. */
+
+ if (errno == errno_quota || errno == ERRNO_EXIMQUOTA || errno == ENOSPC)
+ {
+ addr->more_errno = 0;
+ if (!isdirectory) addr->more_errno = (int)(time(NULL) - times.actime);
+
+ #ifdef SUPPORT_MAILDIR
+ else if (mbformat == mbf_maildir)
+ {
+ struct stat statbuf;
+ if (Ustat("new", &statbuf) < 0)
+ {
+ DEBUG(D_transport) debug_printf("maildir quota exceeded: "
+ "stat error %d for \"new\": %s\n", errno, strerror(errno));
+ }
+ else /* Want a repeatable time when in test harness */
+ addr->more_errno = f.running_in_test_harness ? 10 :
+ (int)time(NULL) - statbuf.st_mtime;
+
+ DEBUG(D_transport)
+ debug_printf("maildir: time since \"new\" directory modified = %s\n",
+ readconf_printtime(addr->more_errno));
+ }
+ #endif /* SUPPORT_MAILDIR */
+ }
+
+ /* Handle system quota excession. Add an explanatory phrase for the error
+ message, since some systems don't have special quota-excession errors,
+ and on those that do, "quota" doesn't always mean anything to the user. */
+
+ if (errno == errno_quota)
+ {
+ #ifndef EDQUOT
+ addr->message = string_sprintf("mailbox is full "
+ "(quota exceeded while writing to file %s)", filename);
+ #else
+ addr->message = US"mailbox is full";
+ #endif /* EDQUOT */
+ addr->user_message = US"mailbox is full";
+ DEBUG(D_transport) debug_printf("System quota exceeded for %s%s%s\n",
+ dataname,
+ isdirectory ? US"" : US": time since file read = ",
+ isdirectory ? US"" : readconf_printtime(addr->more_errno));
+ }
+
+ /* Handle Exim's own quota-imposition */
+
+ else if (errno == ERRNO_EXIMQUOTA)
+ {
+ addr->message = string_sprintf("mailbox is full "
+ "(MTA-imposed%s quota exceeded while writing to %s)", filecount_msg,
+ dataname);
+ addr->user_message = US"mailbox is full";
+ DEBUG(D_transport) debug_printf("Exim%s quota exceeded for %s%s%s\n",
+ filecount_msg, dataname,
+ isdirectory ? US"" : US": time since file read = ",
+ isdirectory ? US"" : readconf_printtime(addr->more_errno));
+ }
+
+ /* Handle a process failure while writing via a filter; the return
+ from child_close() is in more_errno. */
+
+ else if (errno == ERRNO_FILTER_FAIL)
+ {
+ yield = PANIC;
+ addr->message = string_sprintf("transport filter process failed (%d) "
+ "while writing to %s%s", addr->more_errno, dataname,
+ (addr->more_errno == EX_EXECFAILED) ? ": unable to execute command" : "");
+ }
+
+ /* Handle failure to expand header changes */
+
+ else if (errno == ERRNO_CHHEADER_FAIL)
+ {
+ yield = PANIC;
+ addr->message =
+ string_sprintf("failed to expand headers_add or headers_remove while "
+ "writing to %s: %s", dataname, expand_string_message);
+ }
+
+ /* Handle failure to complete writing of a data block */
+
+ else if (errno == ERRNO_WRITEINCOMPLETE)
+ addr->message = string_sprintf("failed to write data block while "
+ "writing to %s", dataname);
+
+ /* Handle length mismatch on MBX copying */
+
+ #ifdef SUPPORT_MBX
+ else if (errno == ERRNO_MBXLENGTH)
+ addr->message = string_sprintf("length mismatch while copying MBX "
+ "temporary file to %s", dataname);
+ #endif /* SUPPORT_MBX */
+
+ /* For other errors, a general-purpose explanation, if the message is
+ not already set. */
+
+ else if (addr->message == NULL)
+ addr->message = string_sprintf("error while writing to %s", dataname);
+
+ /* For a file, reset the file size to what it was before we started, leaving
+ the last modification time unchanged, so it will get reset also. All systems
+ investigated so far have ftruncate(), whereas not all have the F_FREESP
+ fcntl() call (BSDI & FreeBSD do not). */
+
+ if (!isdirectory && ftruncate(fd, saved_size))
+ DEBUG(D_transport) debug_printf("Error resetting file size\n");
+ }
+
+/* Handle successful writing - we want the modification time to be now for
+appended files. Remove the default backstop error number. For a directory, now
+is the time to rename the file with a unique name. As soon as such a name
+appears it may get used by another process, so we close the file first and
+check that all is well. */
+
+else
+ {
+ times.modtime = time(NULL);
+ addr->basic_errno = 0;
+
+ /* Handle the case of writing to a new file in a directory. This applies
+ to all single-file formats - maildir, mailstore, and "smail format". */
+
+ if (isdirectory)
+ {
+ if (fstat(fd, &statbuf) < 0)
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while fstatting opened message file %s",
+ filename);
+ yield = DEFER;
+ }
+
+ else if (close(fd) < 0)
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("close() error for %s",
+ (ob->mailstore_format) ? dataname : filename);
+ yield = DEFER;
+ }
+
+ /* File is successfully written and closed. Arrange to rename it. For the
+ different kinds of single-file delivery, some games can be played with the
+ name. The message size is by this time set to the accurate value so that
+ its value can be used in expansions. */
+
+ else
+ {
+ uschar *renamename = newname;
+ fd = -1;
+
+ DEBUG(D_transport) debug_printf("renaming temporary file\n");
+
+ /* If there is no rename name set, we are in a non-maildir, non-mailstore
+ situation. The name is built by expanding the directory_file option, and
+ we make the inode number available for use in this. The expansion was
+ checked for syntactic validity above, before we wrote the file.
+
+ We have to be careful here, in case the file name exists. (In the other
+ cases, the names used are constructed to be unique.) The rename()
+ function just replaces an existing file - we don't want that! So instead
+ of calling rename(), we must use link() and unlink().
+
+ In this case, if the link fails because of an existing file, we wait
+ for one second and try the expansion again, to see if it produces a
+ different value. Do this up to 5 times unless the name stops changing.
+ This makes it possible to build values that are based on the time, and
+ still cope with races from multiple simultaneous deliveries. */
+
+ if (!newname)
+ {
+ uschar *renameleaf;
+ uschar *old_renameleaf = US"";
+
+ for (int i = 0; ; sleep(1), i++)
+ {
+ deliver_inode = statbuf.st_ino;
+ renameleaf = expand_string(ob->dirfilename);
+ deliver_inode = 0;
+
+ if (!renameleaf)
+ {
+ addr->transport_return = PANIC;
+ addr->message = string_sprintf("Expansion of \"%s\" "
+ "(directory_file for %s transport) failed: %s",
+ ob->dirfilename, tblock->name, expand_string_message);
+ goto RETURN;
+ }
+
+ renamename = string_sprintf("%s/%s", path, renameleaf);
+ if (Ulink(filename, renamename) < 0)
+ {
+ DEBUG(D_transport) debug_printf("link failed: %s\n",
+ strerror(errno));
+ if (errno != EEXIST || i >= 4 ||
+ Ustrcmp(renameleaf, old_renameleaf) == 0)
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while renaming %s as %s",
+ filename, renamename);
+ yield = DEFER;
+ break;
+ }
+ old_renameleaf = renameleaf;
+ DEBUG(D_transport) debug_printf("%s exists - trying again\n",
+ renamename);
+ }
+ else
+ {
+ Uunlink(filename);
+ filename = NULL;
+ break;
+ }
+ } /* re-expand loop */
+ } /* not mailstore or maildir */
+
+ /* For maildir and mailstore formats, the new name was created earlier,
+ except that for maildir, there is the possibility of adding a "tag" on
+ the end of the name by expanding the value of nametag. This usually
+ includes a reference to the message size. The expansion of nametag was
+ checked above, before the file was opened. It either succeeded, or
+ provoked a soft failure. So any failure here can be treated as soft.
+ Ignore non-printing characters and / and put a colon at the start if the
+ first character is alphanumeric. */
+
+ else
+ {
+ if (nametag)
+ {
+ uschar *iptr = expand_string(nametag);
+ if (iptr)
+ {
+ uschar *etag = store_get(Ustrlen(iptr) + 2, iptr);
+ uschar *optr = etag;
+ for ( ; *iptr; iptr++)
+ if (mac_isgraph(*iptr) && *iptr != '/')
+ {
+ if (optr == etag && isalnum(*iptr)) *optr++ = ':';
+ *optr++ = *iptr;
+ }
+ *optr = 0;
+ renamename = string_sprintf("%s%s", newname, etag);
+ }
+ }
+
+ /* Do the rename. If the name is too long and a tag exists, try again
+ without the tag. */
+
+ if (Urename(filename, renamename) < 0 &&
+ (nametag == NULL || errno != ENAMETOOLONG ||
+ (renamename = newname, Urename(filename, renamename) < 0)))
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while renaming %s as %s",
+ filename, renamename);
+ yield = DEFER;
+ }
+
+ /* Rename succeeded */
+
+ else
+ {
+ DEBUG(D_transport) debug_printf("renamed %s as %s\n", filename,
+ renamename);
+ filename = dataname = NULL; /* Prevents attempt to unlink at end */
+ }
+ } /* maildir or mailstore */
+ } /* successful write + close */
+ } /* isdirectory */
+ } /* write success */
+
+
+/* For a file, restore the last access time (atime), and set the modification
+time as required - changed if write succeeded, unchanged if not. */
+
+if (!isdirectory) utime(CS filename, &times);
+
+/* Notify comsat if configured to do so. It only makes sense if the configured
+file is the one that the comsat daemon knows about. */
+
+if (ob->notify_comsat && yield == OK && deliver_localpart)
+ notify_comsat(deliver_localpart, saved_size);
+
+/* Pass back the final return code in the address structure */
+
+DEBUG(D_transport)
+ debug_printf("appendfile yields %d with errno=%d more_errno=%d\n",
+ yield, addr->basic_errno, addr->more_errno);
+
+addr->transport_return = yield;
+
+/* Close the file, which will release the fcntl lock. For a directory write it
+is closed above, except in cases of error which goto RETURN, when we also need
+to remove the original file(s). For MBX locking, if all has gone well, before
+closing the file, see if we can get an exclusive lock on it, in which case we
+can unlink the /tmp lock file before closing it. This is always a non-blocking
+lock; there's no need to wait if we can't get it. If everything has gone right
+but close fails, defer the message. Then unlink the lock file, if present. This
+point in the code is jumped to from a number of places when errors are
+detected, in order to get the file closed and the lock file tidied away. */
+
+RETURN:
+
+#ifdef SUPPORT_MBX
+if (mbx_lockfd >= 0)
+ {
+ if (yield == OK && apply_lock(fd, F_WRLCK, ob->use_fcntl, 0,
+ ob->use_flock, 0) >= 0)
+ {
+ DEBUG(D_transport)
+ debug_printf("unlinking MBX lock file %s\n", mbx_lockname);
+ Uunlink(mbx_lockname);
+ }
+ (void)close(mbx_lockfd);
+ }
+#endif /* SUPPORT_MBX */
+
+if (fd >= 0 && close(fd) < 0 && yield == OK)
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while closing %s", filename);
+ addr->transport_return = DEFER;
+ }
+
+if (hd >= 0) Uunlink(lockname);
+
+/* We get here with isdirectory and filename set only in error situations. */
+
+if (isdirectory && filename)
+ {
+ Uunlink(filename);
+ if (dataname != filename) Uunlink(dataname);
+ }
+
+/* If wait_for_tick is TRUE, we have done a delivery where the uniqueness of a
+file name relies on time + pid. We must not allow the process to finish until
+the clock has move on by at least one microsecond. Usually we expect this
+already to be the case, but machines keep getting faster... */
+
+if (wait_for_tick) exim_wait_tick(&msg_tv, 1);
+
+/* A return of FALSE means that if there was an error, a common error was
+put in the first address of a batch. */
+
+return FALSE;
+
+tainted_ret_panic:
+ addr->message = string_sprintf("Tainted '%s' (file or directory "
+ "name for %s transport) not permitted", path, tblock->name);
+ret_panic:
+ addr->transport_return = PANIC;
+ return FALSE;
+}
+
+#endif /*!MACRO_PREDEF*/
+/* End of transport/appendfile.c */
diff --git a/src/transports/appendfile.h b/src/transports/appendfile.h
new file mode 100644
index 0000000..3fd2f46
--- /dev/null
+++ b/src/transports/appendfile.h
@@ -0,0 +1,100 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) University of Cambridge 1995 - 2018 */
+/* Copyright (c) The Exim Maintainers 2021 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+/* Private structure for the private options. */
+
+typedef struct {
+ uschar *filename;
+ uschar *dirname;
+ uschar *dirfilename;
+ uschar *message_prefix;
+ uschar *message_suffix;
+ uschar *create_file_string;
+ uschar *quota;
+ uschar *quota_directory;
+ uschar *quota_filecount;
+ uschar *quota_size_regex;
+ uschar *quota_warn_threshold;
+ uschar *mailbox_size_string;
+ uschar *mailbox_filecount_string;
+ uschar *expand_maildir_use_size_file;
+ uschar *maildir_dir_regex;
+ uschar *maildir_tag;
+ uschar *maildirfolder_create_regex;
+ uschar *mailstore_prefix;
+ uschar *mailstore_suffix;
+ uschar *check_string;
+ uschar *escape_string;
+ uschar *file_format;
+ off_t quota_value;
+ off_t quota_warn_threshold_value;
+ off_t mailbox_size_value;
+ int mailbox_filecount_value;
+ int quota_filecount_value;
+ int mode;
+ int dirmode;
+ int lockfile_mode;
+ int lockfile_timeout;
+ int lock_fcntl_timeout;
+ int lock_flock_timeout;
+ int lock_retries;
+ int lock_interval;
+ int maildir_retries;
+ int create_file;
+ int options;
+ BOOL allow_fifo;
+ BOOL allow_symlink;
+ BOOL check_group;
+ BOOL check_owner;
+ BOOL create_directory;
+ BOOL notify_comsat;
+ BOOL use_lockfile;
+ BOOL set_use_lockfile;
+ BOOL use_fcntl;
+ BOOL set_use_fcntl;
+ BOOL use_flock;
+ BOOL set_use_flock;
+ BOOL use_mbx_lock;
+ BOOL set_use_mbx_lock;
+ BOOL use_bsmtp;
+ BOOL use_crlf;
+ BOOL file_must_exist;
+ BOOL mode_fail_narrower;
+ BOOL maildir_format;
+ BOOL maildir_use_size_file;
+ BOOL mailstore_format;
+ BOOL mbx_format;
+ BOOL quota_warn_threshold_is_percent;
+ BOOL quota_is_inclusive;
+ BOOL quota_no_check;
+ BOOL quota_filecount_no_check;
+} appendfile_transport_options_block;
+
+/* Restricted creation options */
+
+enum { create_anywhere, create_belowhome, create_inhome };
+
+/* Data for reading the private options. */
+
+extern optionlist appendfile_transport_options[];
+extern int appendfile_transport_options_count;
+
+/* Block containing default values. */
+
+extern appendfile_transport_options_block appendfile_transport_option_defaults;
+
+/* The main and init entry points for the transport */
+
+extern BOOL appendfile_transport_entry(transport_instance *, address_item *);
+extern void appendfile_transport_init(transport_instance *);
+
+/* Function that is shared with tf_maildir.c */
+
+extern off_t check_dir_size(const uschar *, int *, const pcre2_code *);
+
+/* End of transports/appendfile.h */
diff --git a/src/transports/autoreply.c b/src/transports/autoreply.c
new file mode 100644
index 0000000..211e328
--- /dev/null
+++ b/src/transports/autoreply.c
@@ -0,0 +1,821 @@
+/*************************************************
+* 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 "autoreply.h"
+
+
+
+/* Options specific to the autoreply transport. They must be in alphabetic
+order (note that "_" comes before the lower case letters). Those starting
+with "*" are not settable by the user but are used by the option-reading
+software for alternative value types. Some options are publicly visible and so
+are stored in the driver instance block. These are flagged with opt_public. */
+#define LOFF(field) OPT_OFF(autoreply_transport_options_block, field)
+
+optionlist autoreply_transport_options[] = {
+ { "bcc", opt_stringptr, LOFF(bcc) },
+ { "cc", opt_stringptr, LOFF(cc) },
+ { "file", opt_stringptr, LOFF(file) },
+ { "file_expand", opt_bool, LOFF(file_expand) },
+ { "file_optional", opt_bool, LOFF(file_optional) },
+ { "from", opt_stringptr, LOFF(from) },
+ { "headers", opt_stringptr, LOFF(headers) },
+ { "log", opt_stringptr, LOFF(logfile) },
+ { "mode", opt_octint, LOFF(mode) },
+ { "never_mail", opt_stringptr, LOFF(never_mail) },
+ { "once", opt_stringptr, LOFF(oncelog) },
+ { "once_file_size", opt_int, LOFF(once_file_size) },
+ { "once_repeat", opt_stringptr, LOFF(once_repeat) },
+ { "reply_to", opt_stringptr, LOFF(reply_to) },
+ { "return_message", opt_bool, LOFF(return_message) },
+ { "subject", opt_stringptr, LOFF(subject) },
+ { "text", opt_stringptr, LOFF(text) },
+ { "to", opt_stringptr, LOFF(to) },
+};
+
+/* Size of the options list. An extern variable has to be used so that its
+address can appear in the tables drtables.c. */
+
+int autoreply_transport_options_count =
+ sizeof(autoreply_transport_options)/sizeof(optionlist);
+
+
+#ifdef MACRO_PREDEF
+
+/* Dummy values */
+autoreply_transport_options_block autoreply_transport_option_defaults = {0};
+void autoreply_transport_init(transport_instance *tblock) {}
+BOOL autoreply_transport_entry(transport_instance *tblock, address_item *addr) {return FALSE;}
+
+#else /*!MACRO_PREDEF*/
+
+
+/* Default private options block for the autoreply transport.
+All non-mentioned lements zero/null/false. */
+
+autoreply_transport_options_block autoreply_transport_option_defaults = {
+ .mode = 0600,
+};
+
+
+
+/* Type of text for the checkexpand() function */
+
+enum { cke_text, cke_hdr, cke_file };
+
+
+
+/*************************************************
+* 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
+autoreply_transport_init(transport_instance *tblock)
+{
+/*
+autoreply_transport_options_block *ob =
+ (autoreply_transport_options_block *)(tblock->options_block);
+*/
+
+/* If a fixed uid field is set, then a gid field must also be set. */
+
+if (tblock->uid_set && !tblock->gid_set && tblock->expand_gid == NULL)
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG,
+ "user set without group for the %s transport", tblock->name);
+}
+
+
+
+
+/*************************************************
+* Expand string and check *
+*************************************************/
+
+/* If the expansion fails, the error is set up in the address. Expanded
+strings must be checked to ensure they contain only printing characters
+and white space. If not, the function fails.
+
+Arguments:
+ s string to expand
+ addr address that is being worked on
+ name transport name, for error text
+ type type, for checking content:
+ cke_text => no check
+ cke_hdr => header, allow \n + whitespace
+ cke_file => file name, no non-printers allowed
+
+Returns: expanded string if expansion succeeds;
+ NULL otherwise
+*/
+
+static uschar *
+checkexpand(uschar *s, address_item *addr, uschar *name, int type)
+{
+uschar *ss = expand_string(s);
+
+if (!ss)
+ {
+ addr->transport_return = FAIL;
+ addr->message = string_sprintf("Expansion of \"%s\" failed in %s transport: "
+ "%s", s, name, expand_string_message);
+ return NULL;
+ }
+
+if (type != cke_text) for (uschar * t = ss; *t != 0; t++)
+ {
+ int c = *t;
+ const uschar * sp;
+ if (mac_isprint(c)) continue;
+ if (type == cke_hdr && c == '\n' && (t[1] == ' ' || t[1] == '\t')) continue;
+ sp = string_printing(s);
+ addr->transport_return = FAIL;
+ addr->message = string_sprintf("Expansion of \"%s\" in %s transport "
+ "contains non-printing character %d", sp, name, c);
+ return NULL;
+ }
+
+return ss;
+}
+
+
+
+
+/*************************************************
+* Check a header line for never_mail *
+*************************************************/
+
+/* This is called to check to, cc, and bcc for addresses in the never_mail
+list. Any that are found are removed.
+
+Arguments:
+ list list of addresses to be checked
+ never_mail an address list, already expanded
+
+Returns: edited replacement address list, or NULL, or original
+*/
+
+static uschar *
+check_never_mail(uschar * list, const uschar * never_mail)
+{
+rmark reset_point = store_mark();
+uschar * newlist = string_copy(list);
+uschar * s = newlist;
+BOOL hit = FALSE;
+
+while (*s)
+ {
+ uschar *error, *next;
+ uschar *e = parse_find_address_end(s, FALSE);
+ int terminator = *e;
+ int start, end, domain, rc;
+
+ /* Temporarily terminate the string at the address end while extracting
+ the operative address within. */
+
+ *e = 0;
+ next = parse_extract_address(s, &error, &start, &end, &domain, FALSE);
+ *e = terminator;
+
+ /* If there is some kind of syntax error, just give up on this header
+ line. */
+
+ if (!next) break;
+
+ /* See if the address is on the never_mail list */
+
+ rc = match_address_list(next, /* address to check */
+ TRUE, /* start caseless */
+ FALSE, /* don't expand the list */
+ &never_mail, /* the list */
+ NULL, /* no caching */
+ -1, /* no expand setup */
+ 0, /* separator from list */
+ NULL); /* no lookup value return */
+
+ if (rc == OK) /* Remove this address */
+ {
+ DEBUG(D_transport)
+ debug_printf("discarding recipient %s (matched never_mail)\n", next);
+ hit = TRUE;
+ if (terminator == ',') e++;
+ memmove(s, e, Ustrlen(e) + 1);
+ }
+ else /* Skip over this address */
+ {
+ s = e;
+ if (terminator == ',') s++;
+ }
+ }
+
+/* If no addresses were removed, retrieve the memory used and return
+the original. */
+
+if (!hit)
+ {
+ store_reset(reset_point);
+ return list;
+ }
+
+/* Check to see if we removed the last address, leaving a terminating comma
+that needs to be removed */
+
+s = newlist + Ustrlen(newlist);
+while (s > newlist && (isspace(s[-1]) || s[-1] == ',')) s--;
+*s = 0;
+
+/* Check to see if there any addresses left; if not, return NULL */
+
+s = newlist;
+while (s && isspace(*s)) s++;
+if (*s)
+ return newlist;
+
+store_reset(reset_point);
+return NULL;
+}
+
+
+
+/*************************************************
+* Main entry point *
+*************************************************/
+
+/* See local README for interface details. This transport always returns
+FALSE, indicating that the top address has the status for all - though in fact
+this transport can handle only one address at at time anyway. */
+
+BOOL
+autoreply_transport_entry(
+ transport_instance *tblock, /* data for this instantiation */
+ address_item *addr) /* address we are working on */
+{
+int fd, pid, rc;
+int cache_fd = -1;
+int cache_size = 0;
+int add_size = 0;
+EXIM_DB * dbm_file = NULL;
+BOOL file_expand, return_message;
+uschar *from, *reply_to, *to, *cc, *bcc, *subject, *headers, *text, *file;
+uschar *logfile, *oncelog;
+uschar *cache_buff = NULL;
+uschar *cache_time = NULL;
+uschar *message_id = NULL;
+header_line *h;
+time_t now = time(NULL);
+time_t once_repeat_sec = 0;
+FILE *fp;
+FILE *ff = NULL;
+
+autoreply_transport_options_block *ob =
+ (autoreply_transport_options_block *)(tblock->options_block);
+
+DEBUG(D_transport) debug_printf("%s transport entered\n", tblock->name);
+
+/* Set up for the good case */
+
+addr->transport_return = OK;
+addr->basic_errno = 0;
+
+/* If the address is pointing to a reply block, then take all the data
+from that block. It has typically been set up by a mail filter processing
+router. Otherwise, the data must be supplied by this transport, and
+it has to be expanded here. */
+
+if (addr->reply)
+ {
+ DEBUG(D_transport) debug_printf("taking data from address\n");
+ from = addr->reply->from;
+ reply_to = addr->reply->reply_to;
+ to = addr->reply->to;
+ cc = addr->reply->cc;
+ bcc = addr->reply->bcc;
+ subject = addr->reply->subject;
+ headers = addr->reply->headers;
+ text = addr->reply->text;
+ file = addr->reply->file;
+ logfile = addr->reply->logfile;
+ oncelog = addr->reply->oncelog;
+ once_repeat_sec = addr->reply->once_repeat;
+ file_expand = addr->reply->file_expand;
+ expand_forbid = addr->reply->expand_forbid;
+ return_message = addr->reply->return_message;
+ }
+else
+ {
+ uschar *oncerepeat = ob->once_repeat;
+
+ DEBUG(D_transport) debug_printf("taking data from transport\n");
+ from = ob->from;
+ reply_to = ob->reply_to;
+ to = ob->to;
+ cc = ob->cc;
+ bcc = ob->bcc;
+ subject = ob->subject;
+ headers = ob->headers;
+ text = ob->text;
+ file = ob->file;
+ logfile = ob->logfile;
+ oncelog = ob->oncelog;
+ file_expand = ob->file_expand;
+ return_message = ob->return_message;
+
+ if ( from && !(from = checkexpand(from, addr, tblock->name, cke_hdr))
+ || reply_to && !(reply_to = checkexpand(reply_to, addr, tblock->name, cke_hdr))
+ || to && !(to = checkexpand(to, addr, tblock->name, cke_hdr))
+ || cc && !(cc = checkexpand(cc, addr, tblock->name, cke_hdr))
+ || bcc && !(bcc = checkexpand(bcc, addr, tblock->name, cke_hdr))
+ || subject && !(subject = checkexpand(subject, addr, tblock->name, cke_hdr))
+ || headers && !(headers = checkexpand(headers, addr, tblock->name, cke_text))
+ || text && !(text = checkexpand(text, addr, tblock->name, cke_text))
+ || file && !(file = checkexpand(file, addr, tblock->name, cke_file))
+ || logfile && !(logfile = checkexpand(logfile, addr, tblock->name, cke_file))
+ || oncelog && !(oncelog = checkexpand(oncelog, addr, tblock->name, cke_file))
+ || oncerepeat && !(oncerepeat = checkexpand(oncerepeat, addr, tblock->name, cke_file))
+ )
+ return FALSE;
+
+ if (oncerepeat)
+ if ((once_repeat_sec = readconf_readtime(oncerepeat, 0, FALSE)) < 0)
+ {
+ addr->transport_return = FAIL;
+ addr->message = string_sprintf("Invalid time value \"%s\" for "
+ "\"once_repeat\" in %s transport", oncerepeat, tblock->name);
+ return FALSE;
+ }
+ }
+
+/* If the never_mail option is set, we have to scan all the recipients and
+remove those that match. */
+
+if (ob->never_mail)
+ {
+ const uschar *never_mail = expand_string(ob->never_mail);
+
+ if (!never_mail)
+ {
+ addr->transport_return = FAIL;
+ addr->message = string_sprintf("Failed to expand \"%s\" for "
+ "\"never_mail\" in %s transport", ob->never_mail, tblock->name);
+ return FALSE;
+ }
+
+ if (to) to = check_never_mail(to, never_mail);
+ if (cc) cc = check_never_mail(cc, never_mail);
+ if (bcc) bcc = check_never_mail(bcc, never_mail);
+
+ if (!to && !cc && !bcc)
+ {
+ DEBUG(D_transport)
+ debug_printf("*** all recipients removed by never_mail\n");
+ return OK;
+ }
+ }
+
+/* If the -N option is set, can't do any more. */
+
+if (f.dont_deliver)
+ {
+ DEBUG(D_transport)
+ debug_printf("*** delivery by %s transport bypassed by -N option\n",
+ tblock->name);
+ return FALSE;
+ }
+
+
+/* If the oncelog field is set, we send want to send only one message to the
+given recipient(s). This works only on the "To" field. If there is no "To"
+field, the message is always sent. If the To: field contains more than one
+recipient, the effect might not be quite as envisaged. If once_file_size is
+set, instead of a dbm file, we use a regular file containing a circular buffer
+recipient cache. */
+
+if (oncelog && *oncelog && to)
+ {
+ time_t then = 0;
+
+ if (is_tainted(oncelog))
+ {
+ addr->transport_return = DEFER;
+ addr->basic_errno = EACCES;
+ addr->message = string_sprintf("Tainted '%s' (once file for %s transport)"
+ " not permitted", oncelog, tblock->name);
+ goto END_OFF;
+ }
+
+ /* Handle fixed-size cache file. */
+
+ if (ob->once_file_size > 0)
+ {
+ uschar * nextp;
+ struct stat statbuf;
+
+ cache_fd = Uopen(oncelog, O_CREAT|O_RDWR, ob->mode);
+ if (cache_fd < 0 || fstat(cache_fd, &statbuf) != 0)
+ {
+ addr->transport_return = DEFER;
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("Failed to %s \"once\" file %s when "
+ "sending message from %s transport: %s",
+ cache_fd < 0 ? "open" : "stat", oncelog, tblock->name, strerror(errno));
+ goto END_OFF;
+ }
+
+ /* Get store in the temporary pool and read the entire file into it. We get
+ an amount of store that is big enough to add the new entry on the end if we
+ need to do that. */
+
+ cache_size = statbuf.st_size;
+ add_size = sizeof(time_t) + Ustrlen(to) + 1;
+ cache_buff = store_get(cache_size + add_size, oncelog);
+
+ if (read(cache_fd, cache_buff, cache_size) != cache_size)
+ {
+ addr->transport_return = DEFER;
+ addr->basic_errno = errno;
+ addr->message = US"error while reading \"once\" file";
+ goto END_OFF;
+ }
+
+ DEBUG(D_transport) debug_printf("%d bytes read from %s\n", cache_size, oncelog);
+
+ /* Scan the data for this recipient. Each entry in the file starts with
+ a time_t sized time value, followed by the address, followed by a binary
+ zero. If we find a match, put the time into "then", and the place where it
+ was found into "cache_time". Otherwise, "then" is left at zero. */
+
+ for (uschar * p = cache_buff; p < cache_buff + cache_size; p = nextp)
+ {
+ uschar *s = p + sizeof(time_t);
+ nextp = s + Ustrlen(s) + 1;
+ if (Ustrcmp(to, s) == 0)
+ {
+ memcpy(&then, p, sizeof(time_t));
+ cache_time = p;
+ break;
+ }
+ }
+ }
+
+ /* Use a DBM file for the list of previous recipients. */
+
+ else
+ {
+ EXIM_DATUM key_datum, result_datum;
+ uschar * dirname, * s;
+
+ dirname = (s = Ustrrchr(oncelog, '/'))
+ ? string_copyn(oncelog, s - oncelog) : NULL;
+ if (!(dbm_file = exim_dbopen(oncelog, dirname, O_RDWR|O_CREAT, ob->mode)))
+ {
+ addr->transport_return = DEFER;
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("Failed to open %s file %s when sending "
+ "message from %s transport: %s", EXIM_DBTYPE, oncelog, tblock->name,
+ strerror(errno));
+ goto END_OFF;
+ }
+
+ exim_datum_init(&key_datum); /* Some DBM libraries need datums */
+ exim_datum_init(&result_datum); /* to be cleared */
+ exim_datum_data_set(&key_datum, (void *) to);
+ exim_datum_size_set(&key_datum, Ustrlen(to) + 1);
+
+ if (exim_dbget(dbm_file, &key_datum, &result_datum))
+ {
+ /* If the datum size is that of a binary time, we are in the new world
+ where messages are sent periodically. Otherwise the file is an old one,
+ where the datum was filled with a tod_log time, which is assumed to be
+ different in size. For that, only one message is ever sent. This change
+ introduced at Exim 3.00. In a couple of years' time the test on the size
+ can be abolished. */
+
+ if (exim_datum_size_get(&result_datum) == sizeof(time_t))
+ memcpy(&then, exim_datum_data_get(&result_datum), sizeof(time_t));
+ else
+ then = now;
+ }
+ }
+
+ /* Either "then" is set zero, if no message has yet been sent, or it
+ is set to the time of the last sending. */
+
+ if (then != 0 && (once_repeat_sec <= 0 || now - then < once_repeat_sec))
+ {
+ int log_fd;
+ if (is_tainted(logfile))
+ {
+ addr->transport_return = DEFER;
+ addr->basic_errno = EACCES;
+ addr->message = string_sprintf("Tainted '%s' (logfile for %s transport)"
+ " not permitted", logfile, tblock->name);
+ goto END_OFF;
+ }
+
+ DEBUG(D_transport) debug_printf("message previously sent to %s%s\n", to,
+ (once_repeat_sec > 0)? " and repeat time not reached" : "");
+ log_fd = logfile ? Uopen(logfile, O_WRONLY|O_APPEND|O_CREAT, ob->mode) : -1;
+ if (log_fd >= 0)
+ {
+ uschar *ptr = log_buffer;
+ sprintf(CS ptr, "%s\n previously sent to %.200s\n", tod_stamp(tod_log), to);
+ while(*ptr) ptr++;
+ if(write(log_fd, log_buffer, ptr - log_buffer) != ptr-log_buffer
+ || close(log_fd))
+ DEBUG(D_transport) debug_printf("Problem writing log file %s for %s "
+ "transport\n", logfile, tblock->name);
+ }
+ goto END_OFF;
+ }
+
+ DEBUG(D_transport) debug_printf("%s %s\n", (then <= 0)?
+ "no previous message sent to" : "repeat time reached for", to);
+ }
+
+/* We are going to send a message. Ensure any requested file is available. */
+if (file)
+ {
+ if (is_tainted(file))
+ {
+ addr->transport_return = DEFER;
+ addr->basic_errno = EACCES;
+ addr->message = string_sprintf("Tainted '%s' (file for %s transport)"
+ " not permitted", file, tblock->name);
+ return FALSE;
+ }
+ if (!(ff = Ufopen(file, "rb")) && !ob->file_optional)
+ {
+ addr->transport_return = DEFER;
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("Failed to open file %s when sending "
+ "message from %s transport: %s", file, tblock->name, strerror(errno));
+ return FALSE;
+ }
+ }
+
+/* Make a subprocess to send the message */
+
+if ((pid = child_open_exim(&fd, US"autoreply")) < 0)
+ {
+ /* Creation of child failed; defer this delivery. */
+
+ addr->transport_return = DEFER;
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("Failed to create child process to send "
+ "message from %s transport: %s", tblock->name, strerror(errno));
+ DEBUG(D_transport) debug_printf("%s\n", addr->message);
+ if (dbm_file) exim_dbclose(dbm_file);
+ return FALSE;
+ }
+
+/* Create the message to be sent - recipients are taken from the headers,
+as the -t option is used. The "headers" stuff *must* be last in case there
+are newlines in it which might, if placed earlier, screw up other headers. */
+
+fp = fdopen(fd, "wb");
+
+if (from) fprintf(fp, "From: %s\n", from);
+if (reply_to) fprintf(fp, "Reply-To: %s\n", reply_to);
+if (to) fprintf(fp, "To: %s\n", to);
+if (cc) fprintf(fp, "Cc: %s\n", cc);
+if (bcc) fprintf(fp, "Bcc: %s\n", bcc);
+if (subject) fprintf(fp, "Subject: %s\n", subject);
+
+/* Generate In-Reply-To from the message_id header; there should
+always be one, but code defensively. */
+
+for (h = header_list; h; h = h->next)
+ if (h->type == htype_id) break;
+
+if (h)
+ {
+ message_id = Ustrchr(h->text, ':') + 1;
+ while (isspace(*message_id)) message_id++;
+ fprintf(fp, "In-Reply-To: %s", message_id);
+ }
+
+moan_write_references(fp, message_id);
+
+/* Add an Auto-Submitted: header */
+
+fprintf(fp, "Auto-Submitted: auto-replied\n");
+
+/* Add any specially requested headers */
+
+if (headers) fprintf(fp, "%s\n", headers);
+fprintf(fp, "\n");
+
+if (text)
+ {
+ fprintf(fp, "%s", CS text);
+ if (text[Ustrlen(text)-1] != '\n') fprintf(fp, "\n");
+ }
+
+if (ff)
+ {
+ while (Ufgets(big_buffer, big_buffer_size, ff) != NULL)
+ {
+ if (file_expand)
+ {
+ uschar *s = expand_string(big_buffer);
+ DEBUG(D_transport)
+ {
+ if (!s)
+ debug_printf("error while expanding line from file:\n %s\n %s\n",
+ big_buffer, expand_string_message);
+ }
+ fprintf(fp, "%s", s ? CS s : CS big_buffer);
+ }
+ else fprintf(fp, "%s", CS big_buffer);
+ }
+ (void) fclose(ff);
+ }
+
+/* Copy the original message if required, observing the return size
+limit if we are returning the body. */
+
+if (return_message)
+ {
+ uschar *rubric = tblock->headers_only
+ ? US"------ This is a copy of the message's header lines.\n"
+ : tblock->body_only
+ ? US"------ This is a copy of the body of the message, without the headers.\n"
+ : US"------ This is a copy of the message, including all the headers.\n";
+ transport_ctx tctx = {
+ .u = {.fd = fileno(fp)},
+ .tblock = tblock,
+ .addr = addr,
+ .check_string = NULL,
+ .escape_string = NULL,
+ .options = (tblock->body_only ? topt_no_headers : 0)
+ | (tblock->headers_only ? topt_no_body : 0)
+ | (tblock->return_path_add ? topt_add_return_path : 0)
+ | (tblock->delivery_date_add ? topt_add_delivery_date : 0)
+ | (tblock->envelope_to_add ? topt_add_envelope_to : 0)
+ | topt_not_socket
+ };
+
+ if (bounce_return_size_limit > 0 && !tblock->headers_only)
+ {
+ struct stat statbuf;
+ int max = (bounce_return_size_limit/DELIVER_IN_BUFFER_SIZE + 1) *
+ DELIVER_IN_BUFFER_SIZE;
+ if (fstat(deliver_datafile, &statbuf) == 0 && statbuf.st_size > max)
+ {
+ fprintf(fp, "\n%s"
+"------ The body of the message is " OFF_T_FMT " characters long; only the first\n"
+"------ %d or so are included here.\n\n", rubric, statbuf.st_size,
+ (max/1000)*1000);
+ }
+ else fprintf(fp, "\n%s\n", rubric);
+ }
+ else fprintf(fp, "\n%s\n", rubric);
+
+ fflush(fp);
+ transport_count = 0;
+ transport_write_message(&tctx, bounce_return_size_limit);
+ }
+
+/* End the message and wait for the child process to end; no timeout. */
+
+(void)fclose(fp);
+rc = child_close(pid, 0);
+
+/* Update the "sent to" log whatever the yield. This errs on the side of
+missing out a message rather than risking sending more than one. We either have
+cache_fd set to a fixed size, circular buffer file, or dbm_file set to an open
+DBM file (or neither, if "once" is not set). */
+
+/* Update fixed-size cache file. If cache_time is set, we found a previous
+entry; that is the spot into which to put the current time. Otherwise we have
+to add a new record; remove the first one in the file if the file is too big.
+We always rewrite the entire file in a single write operation. This is
+(hopefully) going to be the safest thing because there is no interlocking
+between multiple simultaneous deliveries. */
+
+if (cache_fd >= 0)
+ {
+ uschar *from = cache_buff;
+ int size = cache_size;
+
+ if (lseek(cache_fd, 0, SEEK_SET) == 0)
+ {
+ if (!cache_time)
+ {
+ cache_time = from + size;
+ memcpy(cache_time + sizeof(time_t), to, add_size - sizeof(time_t));
+ size += add_size;
+
+ if (cache_size > 0 && size > ob->once_file_size)
+ {
+ from += sizeof(time_t) + Ustrlen(from + sizeof(time_t)) + 1;
+ size -= (from - cache_buff);
+ }
+ }
+
+ memcpy(cache_time, &now, sizeof(time_t));
+ if(write(cache_fd, from, size) != size)
+ DEBUG(D_transport) debug_printf("Problem writing cache file %s for %s "
+ "transport\n", oncelog, tblock->name);
+ }
+ }
+
+/* Update DBM file */
+
+else if (dbm_file)
+ {
+ EXIM_DATUM key_datum, value_datum;
+ exim_datum_init(&key_datum); /* Some DBM libraries need to have */
+ exim_datum_init(&value_datum); /* cleared datums. */
+ exim_datum_data_set(&key_datum, to);
+ exim_datum_size_set(&key_datum, Ustrlen(to) + 1);
+
+ /* Many OS define the datum value, sensibly, as a void *. However, there
+ are some which still have char *. By casting this address to a char * we
+ can avoid warning messages from the char * systems. */
+
+ exim_datum_data_set(&value_datum, &now);
+ exim_datum_size_set(&value_datum, sizeof(time_t));
+ exim_dbput(dbm_file, &key_datum, &value_datum);
+ }
+
+/* If sending failed, defer to try again - but if once is set the next
+try will skip, of course. However, if there were no recipients in the
+message, we do not fail. */
+
+if (rc != 0)
+ if (rc == EXIT_NORECIPIENTS)
+ {
+ DEBUG(D_any) debug_printf("%s transport: message contained no recipients\n",
+ tblock->name);
+ }
+ else
+ {
+ addr->transport_return = DEFER;
+ addr->message = string_sprintf("Failed to send message from %s "
+ "transport (%d)", tblock->name, rc);
+ goto END_OFF;
+ }
+
+/* Log the sending of the message if successful and required. If the file
+fails to open, it's hard to know what to do. We cannot write to the Exim
+log from here, since we may be running under an unprivileged uid. We don't
+want to fail the delivery, since the message has been successfully sent. For
+the moment, ignore open failures. Write the log entry as a single write() to a
+file opened for appending, in order to avoid interleaving of output from
+different processes. The log_buffer can be used exactly as for main log
+writing. */
+
+if (logfile)
+ {
+ int log_fd = Uopen(logfile, O_WRONLY|O_APPEND|O_CREAT, ob->mode);
+ if (log_fd >= 0)
+ {
+ gstring gs = { .size = LOG_BUFFER_SIZE, .ptr = 0, .s = log_buffer }, *g = &gs;
+
+ /* Use taint-unchecked routines for writing into log_buffer, trusting
+ that we'll never expand it. */
+
+ DEBUG(D_transport) debug_printf("logging message details\n");
+ g = string_fmt_append_f(g, SVFMT_TAINT_NOCHK, "%s\n", tod_stamp(tod_log));
+ if (from)
+ g = string_fmt_append_f(g, SVFMT_TAINT_NOCHK, " From: %s\n", from);
+ if (to)
+ g = string_fmt_append_f(g, SVFMT_TAINT_NOCHK, " To: %s\n", to);
+ if (cc)
+ g = string_fmt_append_f(g, SVFMT_TAINT_NOCHK, " Cc: %s\n", cc);
+ if (bcc)
+ g = string_fmt_append_f(g, SVFMT_TAINT_NOCHK, " Bcc: %s\n", bcc);
+ if (subject)
+ g = string_fmt_append_f(g, SVFMT_TAINT_NOCHK, " Subject: %s\n", subject);
+ if (headers)
+ g = string_fmt_append_f(g, SVFMT_TAINT_NOCHK, " %s\n", headers);
+ if(write(log_fd, g->s, g->ptr) != g->ptr || close(log_fd))
+ DEBUG(D_transport) debug_printf("Problem writing log file %s for %s "
+ "transport\n", logfile, tblock->name);
+ }
+ else DEBUG(D_transport) debug_printf("Failed to open log file %s for %s "
+ "transport: %s\n", logfile, tblock->name, strerror(errno));
+ }
+
+END_OFF:
+if (dbm_file) exim_dbclose(dbm_file);
+if (cache_fd > 0) (void)close(cache_fd);
+
+DEBUG(D_transport) debug_printf("%s transport succeeded\n", tblock->name);
+
+return FALSE;
+}
+
+#endif /*!MACRO_PREDEF*/
+/* End of transport/autoreply.c */
diff --git a/src/transports/autoreply.h b/src/transports/autoreply.h
new file mode 100644
index 0000000..fcfd981
--- /dev/null
+++ b/src/transports/autoreply.h
@@ -0,0 +1,45 @@
+/*************************************************
+* 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 *from;
+ uschar *reply_to;
+ uschar *to;
+ uschar *cc;
+ uschar *bcc;
+ uschar *subject;
+ uschar *headers;
+ uschar *text;
+ uschar *file;
+ uschar *logfile;
+ uschar *oncelog;
+ uschar *once_repeat;
+ uschar *never_mail;
+ int mode;
+ off_t once_file_size;
+ BOOL file_expand;
+ BOOL file_optional;
+ BOOL return_message;
+} autoreply_transport_options_block;
+
+/* Data for reading the private options. */
+
+extern optionlist autoreply_transport_options[];
+extern int autoreply_transport_options_count;
+
+/* Block containing default values. */
+
+extern autoreply_transport_options_block autoreply_transport_option_defaults;
+
+/* The main and init entry points for the transport */
+
+extern BOOL autoreply_transport_entry(transport_instance *, address_item *);
+extern void autoreply_transport_init(transport_instance *);
+
+/* End of transports/autoreply.h */
diff --git a/src/transports/lmtp.c b/src/transports/lmtp.c
new file mode 100644
index 0000000..f751771
--- /dev/null
+++ b/src/transports/lmtp.c
@@ -0,0 +1,809 @@
+/*************************************************
+* 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 "lmtp.h"
+
+#define PENDING_OK 256
+
+
+/* Options specific to the lmtp transport. They must be in alphabetic
+order (note that "_" comes before the lower case letters). Those starting
+with "*" are not settable by the user but are used by the option-reading
+software for alternative value types. Some options are stored in the transport
+instance block so as to be publicly visible; these are flagged with opt_public.
+*/
+
+optionlist lmtp_transport_options[] = {
+ { "batch_id", opt_stringptr | opt_public,
+ OPT_OFF(transport_instance, batch_id) },
+ { "batch_max", opt_int | opt_public,
+ OPT_OFF(transport_instance, batch_max) },
+ { "command", opt_stringptr,
+ OPT_OFF(lmtp_transport_options_block, cmd) },
+ { "ignore_quota", opt_bool,
+ OPT_OFF(lmtp_transport_options_block, ignore_quota) },
+ { "socket", opt_stringptr,
+ OPT_OFF(lmtp_transport_options_block, skt) },
+ { "timeout", opt_time,
+ OPT_OFF(lmtp_transport_options_block, timeout) }
+};
+
+/* Size of the options list. An extern variable has to be used so that its
+address can appear in the tables drtables.c. */
+
+int lmtp_transport_options_count =
+ sizeof(lmtp_transport_options)/sizeof(optionlist);
+
+
+#ifdef MACRO_PREDEF
+
+/* Dummy values */
+lmtp_transport_options_block lmtp_transport_option_defaults = {0};
+void lmtp_transport_init(transport_instance *tblock) {}
+BOOL lmtp_transport_entry(transport_instance *tblock, address_item *addr) {return FALSE;}
+
+#else /*!MACRO_PREDEF*/
+
+
+/* Default private options block for the lmtp transport. */
+
+lmtp_transport_options_block lmtp_transport_option_defaults = {
+ NULL, /* cmd */
+ NULL, /* skt */
+ 5*60, /* timeout */
+ 0, /* options */
+ FALSE /* ignore_quota */
+};
+
+
+
+/*************************************************
+* 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
+lmtp_transport_init(transport_instance *tblock)
+{
+lmtp_transport_options_block *ob =
+ (lmtp_transport_options_block *)(tblock->options_block);
+
+/* Either the command field or the socket field must be set */
+
+if ((ob->cmd == NULL) == (ob->skt == NULL))
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG,
+ "one (and only one) of command or socket must be set for the %s transport",
+ tblock->name);
+
+/* If a fixed uid field is set, then a gid field must also be set. */
+
+if (tblock->uid_set && !tblock->gid_set && tblock->expand_gid == NULL)
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG,
+ "user set without group for the %s transport", tblock->name);
+
+/* Set up the bitwise options for transport_write_message from the various
+driver options. Only one of body_only and headers_only can be set. */
+
+ob->options |=
+ (tblock->body_only? topt_no_headers : 0) |
+ (tblock->headers_only? topt_no_body : 0) |
+ (tblock->return_path_add? topt_add_return_path : 0) |
+ (tblock->delivery_date_add? topt_add_delivery_date : 0) |
+ (tblock->envelope_to_add? topt_add_envelope_to : 0) |
+ topt_use_crlf | topt_end_dot;
+}
+
+
+/*************************************************
+* Check an LMTP response *
+*************************************************/
+
+/* This function is given an errno code and the LMTP response buffer to
+analyse. It sets an appropriate message and puts the first digit of the
+response code into the yield variable. If no response was actually read, a
+suitable digit is chosen.
+
+Arguments:
+ errno_value pointer to the errno value
+ more_errno from the top address for use with ERRNO_FILTER_FAIL
+ buffer the LMTP response buffer
+ yield where to put a one-digit LMTP response code
+ message where to put an error message
+
+Returns: TRUE if a "QUIT" command should be sent, else FALSE
+*/
+
+static BOOL
+check_response(int *errno_value, int more_errno, uschar *buffer,
+ int *yield, uschar **message)
+{
+*yield = '4'; /* Default setting is to give a temporary error */
+
+/* Handle response timeout */
+
+if (*errno_value == ETIMEDOUT)
+ {
+ *message = string_sprintf("LMTP timeout after %s", big_buffer);
+ if (transport_count > 0)
+ *message = string_sprintf("%s (%d bytes written)", *message,
+ transport_count);
+ *errno_value = 0;
+ return FALSE;
+ }
+
+/* Handle malformed LMTP response */
+
+if (*errno_value == ERRNO_SMTPFORMAT)
+ {
+ *message = string_sprintf("Malformed LMTP response after %s: %s",
+ big_buffer, string_printing(buffer));
+ return FALSE;
+ }
+
+/* Handle a failed filter process error; can't send QUIT as we mustn't
+end the DATA. */
+
+if (*errno_value == ERRNO_FILTER_FAIL)
+ {
+ *message = string_sprintf("transport filter process failed (%d)%s",
+ more_errno,
+ (more_errno == EX_EXECFAILED)? ": unable to execute command" : "");
+ return FALSE;
+ }
+
+/* Handle a failed add_headers expansion; can't send QUIT as we mustn't
+end the DATA. */
+
+if (*errno_value == ERRNO_CHHEADER_FAIL)
+ {
+ *message =
+ string_sprintf("failed to expand headers_add or headers_remove: %s",
+ expand_string_message);
+ return FALSE;
+ }
+
+/* Handle failure to write a complete data block */
+
+if (*errno_value == ERRNO_WRITEINCOMPLETE)
+ {
+ *message = US"failed to write a data block";
+ return FALSE;
+ }
+
+/* Handle error responses from the remote process. */
+
+if (buffer[0] != 0)
+ {
+ const uschar *s = string_printing(buffer);
+ *message = string_sprintf("LMTP error after %s: %s", big_buffer, s);
+ *yield = buffer[0];
+ return TRUE;
+ }
+
+/* No data was read. If there is no errno, this must be the EOF (i.e.
+connection closed) case, which causes deferral. Otherwise, leave the errno
+value to be interpreted. In all cases, we have to assume the connection is now
+dead. */
+
+if (*errno_value == 0)
+ {
+ *errno_value = ERRNO_SMTPCLOSED;
+ *message = string_sprintf("LMTP connection closed after %s", big_buffer);
+ }
+
+return FALSE;
+}
+
+
+
+/*************************************************
+* Write LMTP command *
+*************************************************/
+
+/* The formatted command is left in big_buffer so that it can be reflected in
+any error message.
+
+Arguments:
+ fd the fd to write to
+ format a format, starting with one of
+ of HELO, MAIL FROM, RCPT TO, DATA, ".", or QUIT.
+ ... data for the format
+
+Returns: TRUE if successful, FALSE if not, with errno set
+*/
+
+static BOOL
+lmtp_write_command(int fd, const char *format, ...)
+{
+gstring gs = { .size = big_buffer_size, .ptr = 0, .s = big_buffer };
+int rc;
+va_list ap;
+
+/*XXX see comment in smtp_write_command() regarding leaving stuff in
+big_buffer */
+
+va_start(ap, format);
+if (!string_vformat(&gs, SVFMT_TAINT_NOCHK, CS format, ap))
+ {
+ va_end(ap);
+ errno = ERRNO_SMTPFORMAT;
+ return FALSE;
+ }
+va_end(ap);
+DEBUG(D_transport|D_v) debug_printf(" LMTP>> %s", string_from_gstring(&gs));
+rc = write(fd, gs.s, gs.ptr);
+gs.ptr -= 2; string_from_gstring(&gs); /* remove \r\n for debug and error message */
+if (rc > 0) return TRUE;
+DEBUG(D_transport) debug_printf("write failed: %s\n", strerror(errno));
+return FALSE;
+}
+
+
+
+
+/*************************************************
+* Read LMTP response *
+*************************************************/
+
+/* This function reads an LMTP response with a timeout, and returns the
+response in the given buffer. It also analyzes the first digit of the reply
+code and returns FALSE if it is not acceptable.
+
+FALSE is also returned after a reading error. In this case buffer[0] will be
+zero, and the error code will be in errno.
+
+Arguments:
+ f a file to read from
+ buffer where to put the response
+ size the size of the buffer
+ okdigit the expected first digit of the response
+ timeout the timeout to use
+
+Returns: TRUE if a valid, non-error response was received; else FALSE
+*/
+
+static BOOL
+lmtp_read_response(FILE *f, uschar *buffer, int size, int okdigit, int timeout)
+{
+int count;
+uschar *ptr = buffer;
+uschar *readptr = buffer;
+
+/* Ensure errno starts out zero */
+
+errno = 0;
+
+/* Loop for handling LMTP responses that do not all come in one line. */
+
+for (;;)
+ {
+ /* If buffer is too full, something has gone wrong. */
+
+ if (size < 10)
+ {
+ *readptr = 0;
+ errno = ERRNO_SMTPFORMAT;
+ return FALSE;
+ }
+
+ /* Loop to cover the read getting interrupted. */
+
+ for (;;)
+ {
+ char *rc;
+ int save_errno;
+
+ *readptr = 0; /* In case nothing gets read */
+ sigalrm_seen = FALSE;
+ ALARM(timeout);
+ rc = Ufgets(readptr, size-1, f);
+ save_errno = errno;
+ ALARM_CLR(0);
+ errno = save_errno;
+
+ if (rc != NULL) break; /* A line has been read */
+
+ /* Handle timeout; must do this first because it uses EINTR */
+
+ if (sigalrm_seen) errno = ETIMEDOUT;
+
+ /* If some other interrupt arrived, just retry. We presume this to be rare,
+ but it can happen (e.g. the SIGUSR1 signal sent by exiwhat causes
+ read() to exit). */
+
+ else if (errno == EINTR)
+ {
+ DEBUG(D_transport) debug_printf("EINTR while reading LMTP response\n");
+ continue;
+ }
+
+ /* Handle other errors, including EOF; ensure buffer is completely empty. */
+
+ buffer[0] = 0;
+ return FALSE;
+ }
+
+ /* Adjust size in case we have to read another line, and adjust the
+ count to be the length of the line we are about to inspect. */
+
+ count = Ustrlen(readptr);
+ size -= count;
+ count += readptr - ptr;
+
+ /* See if the final two characters in the buffer are \r\n. If not, we
+ have to read some more. At least, that is what we should do on a strict
+ interpretation of the RFC. But accept LF as well, as we do for SMTP. */
+
+ if (ptr[count-1] != '\n')
+ {
+ DEBUG(D_transport)
+ {
+ debug_printf("LMTP input line incomplete in one buffer:\n ");
+ for (int i = 0; i < count; i++)
+ {
+ int c = (ptr[i]);
+ if (mac_isprint(c)) debug_printf("%c", c); else debug_printf("<%d>", c);
+ }
+ debug_printf("\n");
+ }
+ readptr = ptr + count;
+ continue;
+ }
+
+ /* Remove any whitespace at the end of the buffer. This gets rid of CR, LF
+ etc. at the end. Show it, if debugging, formatting multi-line responses. */
+
+ while (count > 0 && isspace(ptr[count-1])) count--;
+ ptr[count] = 0;
+
+ DEBUG(D_transport|D_v)
+ {
+ uschar *s = ptr;
+ uschar *t = ptr;
+ while (*t != 0)
+ {
+ while (*t != 0 && *t != '\n') t++;
+ debug_printf(" %s %*s\n", (s == ptr)? "LMTP<<" : " ",
+ (int)(t-s), s);
+ if (*t == 0) break;
+ s = t = t + 1;
+ }
+ }
+
+ /* Check the format of the response: it must start with three digits; if
+ these are followed by a space or end of line, the response is complete. If
+ they are followed by '-' this is a multi-line response and we must look for
+ another line until the final line is reached. The only use made of multi-line
+ responses is to pass them back as error messages. We therefore just
+ concatenate them all within the buffer, which should be large enough to
+ accept any reasonable number of lines. A multiline response may already
+ have been read in one go - hence the loop here. */
+
+ for(;;)
+ {
+ uschar *p;
+ if (count < 3 ||
+ !isdigit(ptr[0]) ||
+ !isdigit(ptr[1]) ||
+ !isdigit(ptr[2]) ||
+ (ptr[3] != '-' && ptr[3] != ' ' && ptr[3] != 0))
+ {
+ errno = ERRNO_SMTPFORMAT; /* format error */
+ return FALSE;
+ }
+
+ /* If a single-line response, exit the loop */
+
+ if (ptr[3] != '-') break;
+
+ /* For a multi-line response see if the next line is already read, and if
+ so, stay in this loop to check it. */
+
+ p = ptr + 3;
+ while (*(++p) != 0)
+ {
+ if (*p == '\n')
+ {
+ ptr = ++p;
+ break;
+ }
+ }
+ if (*p == 0) break; /* No more lines to check */
+ }
+
+ /* End of response. If the last of the lines we are looking at is the final
+ line, we are done. Otherwise more data has to be read. */
+
+ if (ptr[3] != '-') break;
+
+ /* Move the reading pointer upwards in the buffer and insert \n in case this
+ is an error message that subsequently gets printed. Set the scanning pointer
+ to the reading pointer position. */
+
+ ptr += count;
+ *ptr++ = '\n';
+ size--;
+ readptr = ptr;
+ }
+
+/* Return a value that depends on the LMTP return code. Ensure that errno is
+zero, because the caller of this function looks at errno when FALSE is
+returned, to distinguish between an unexpected return code and other errors
+such as timeouts, lost connections, etc. */
+
+errno = 0;
+return buffer[0] == okdigit;
+}
+
+
+
+
+
+
+/*************************************************
+* Main entry point *
+*************************************************/
+
+/* See local README for interface details. For setup-errors, this transport
+returns FALSE, indicating that the first address has the status for all; in
+normal cases it returns TRUE, indicating that each address has its own status
+set. */
+
+BOOL
+lmtp_transport_entry(
+ transport_instance *tblock, /* data for this instantiation */
+ address_item *addrlist) /* address(es) we are working on */
+{
+pid_t pid = 0;
+FILE *out;
+lmtp_transport_options_block *ob =
+ (lmtp_transport_options_block *)(tblock->options_block);
+struct sockaddr_un sockun; /* don't call this "sun" ! */
+int timeout = ob->timeout;
+int fd_in = -1, fd_out = -1;
+int code, save_errno;
+BOOL send_data;
+BOOL yield = FALSE;
+uschar *igquotstr = US"";
+uschar *sockname = NULL;
+const uschar **argv;
+uschar buffer[256];
+
+DEBUG(D_transport) debug_printf("%s transport entered\n", tblock->name);
+
+/* Initialization ensures that either a command or a socket is specified, but
+not both. When a command is specified, call the common function for creating an
+argument list and expanding the items. */
+
+if (ob->cmd)
+ {
+ DEBUG(D_transport) debug_printf("using command %s\n", ob->cmd);
+ sprintf(CS buffer, "%.50s transport", tblock->name);
+ if (!transport_set_up_command(&argv, ob->cmd, TRUE, PANIC, addrlist, FALSE,
+ buffer, NULL))
+ return FALSE;
+
+ /* If the -N option is set, can't do any more. Presume all has gone well. */
+ if (f.dont_deliver)
+ goto MINUS_N;
+
+/* As this is a local transport, we are already running with the required
+uid/gid and current directory. Request that the new process be a process group
+leader, so we can kill it and all its children on an error. */
+
+ if ((pid = child_open(USS argv, NULL, 0, &fd_in, &fd_out, TRUE,
+ US"lmtp-tpt-cmd")) < 0)
+ {
+ addrlist->message = string_sprintf(
+ "Failed to create child process for %s transport: %s", tblock->name,
+ strerror(errno));
+ return FALSE;
+ }
+ }
+
+/* When a socket is specified, expand the string and create a socket. */
+
+else
+ {
+ DEBUG(D_transport) debug_printf("using socket %s\n", ob->skt);
+ if (!(sockname = expand_string(ob->skt)))
+ {
+ addrlist->message = string_sprintf("Expansion of \"%s\" (socket setting "
+ "for %s transport) failed: %s", ob->skt, tblock->name,
+ expand_string_message);
+ return FALSE;
+ }
+ if ((fd_in = fd_out = socket(PF_UNIX, SOCK_STREAM, 0)) == -1)
+ {
+ addrlist->message = string_sprintf(
+ "Failed to create socket %s for %s transport: %s",
+ ob->skt, tblock->name, strerror(errno));
+ return FALSE;
+ }
+
+ /* If the -N option is set, can't do any more. Presume all has gone well. */
+ if (f.dont_deliver)
+ goto MINUS_N;
+
+ sockun.sun_family = AF_UNIX;
+ sprintf(sockun.sun_path, "%.*s", (int)(sizeof(sockun.sun_path)-1), sockname);
+ if(connect(fd_out, (struct sockaddr *)(&sockun), sizeof(sockun)) == -1)
+ {
+ addrlist->message = string_sprintf(
+ "Failed to connect to socket %s for %s transport: %s",
+ sockun.sun_path, tblock->name, strerror(errno));
+ return FALSE;
+ }
+ }
+
+
+/* Make the output we are going to read into a file. */
+
+out = fdopen(fd_out, "rb");
+
+/* Now we must implement the LMTP protocol. It is like SMTP, except that after
+the end of the message, a return code for every accepted RCPT TO is sent. This
+allows for message+recipient checks after the message has been received. */
+
+/* First thing is to wait for an initial greeting. */
+
+Ustrcpy(big_buffer, US"initial connection");
+if (!lmtp_read_response(out, buffer, sizeof(buffer), '2', timeout))
+ goto RESPONSE_FAILED;
+
+/* Next, we send a LHLO command, and expect a positive response */
+
+if (!lmtp_write_command(fd_in, "%s %s\r\n", "LHLO", primary_hostname))
+ goto WRITE_FAILED;
+
+if (!lmtp_read_response(out, buffer, sizeof(buffer), '2', timeout))
+ goto RESPONSE_FAILED;
+
+/* If the ignore_quota option is set, note whether the server supports the
+IGNOREQUOTA option, and if so, set an appropriate addition for RCPT. */
+
+if (ob->ignore_quota)
+ igquotstr = regex_match(regex_IGNOREQUOTA, buffer, -1, NULL)
+ ? US" IGNOREQUOTA" : US"";
+
+/* Now the envelope sender */
+
+if (!lmtp_write_command(fd_in, "MAIL FROM:<%s>\r\n", return_path))
+ goto WRITE_FAILED;
+
+if (!lmtp_read_response(out, buffer, sizeof(buffer), '2', timeout))
+ {
+ if (errno == 0 && buffer[0] == '4')
+ {
+ errno = ERRNO_MAIL4XX;
+ addrlist->more_errno |= ((buffer[1] - '0')*10 + buffer[2] - '0') << 8;
+ }
+ goto RESPONSE_FAILED;
+ }
+
+/* Next, we hand over all the recipients. Some may be permanently or
+temporarily rejected; others may be accepted, for now. */
+
+send_data = FALSE;
+for (address_item * addr = addrlist; addr; addr = addr->next)
+ {
+ if (!lmtp_write_command(fd_in, "RCPT TO:<%s>%s\r\n",
+ transport_rcpt_address(addr, tblock->rcpt_include_affixes), igquotstr))
+ goto WRITE_FAILED;
+ if (lmtp_read_response(out, buffer, sizeof(buffer), '2', timeout))
+ {
+ send_data = TRUE;
+ addr->transport_return = PENDING_OK;
+ }
+ else
+ {
+ if (errno != 0 || buffer[0] == 0) goto RESPONSE_FAILED;
+ addr->message = string_sprintf("LMTP error after %s: %s", big_buffer,
+ string_printing(buffer));
+ setflag(addr, af_pass_message); /* Allow message to go to user */
+ if (buffer[0] == '5') addr->transport_return = FAIL; else
+ {
+ addr->basic_errno = ERRNO_RCPT4XX;
+ addr->more_errno |= ((buffer[1] - '0')*10 + buffer[2] - '0') << 8;
+ }
+ }
+ }
+
+/* Now send the text of the message if there were any good recipients. */
+
+if (send_data)
+ {
+ BOOL ok;
+ transport_ctx tctx = {
+ {fd_in},
+ tblock,
+ addrlist,
+ US".", US"..",
+ ob->options
+ };
+
+ if (!lmtp_write_command(fd_in, "DATA\r\n")) goto WRITE_FAILED;
+ if (!lmtp_read_response(out, buffer, sizeof(buffer), '3', timeout))
+ {
+ if (errno == 0 && buffer[0] == '4')
+ {
+ errno = ERRNO_DATA4XX;
+ addrlist->more_errno |= ((buffer[1] - '0')*10 + buffer[2] - '0') << 8;
+ }
+ goto RESPONSE_FAILED;
+ }
+
+ sigalrm_seen = FALSE;
+ transport_write_timeout = timeout;
+ Ustrcpy(big_buffer, US"sending data block"); /* For error messages */
+ DEBUG(D_transport|D_v)
+ debug_printf(" LMTP>> writing message and terminating \".\"\n");
+
+ transport_count = 0;
+ ok = transport_write_message(&tctx, 0);
+
+ /* Failure can either be some kind of I/O disaster (including timeout),
+ or the failure of a transport filter or the expansion of added headers. */
+
+ if (!ok)
+ {
+ buffer[0] = 0; /* There hasn't been a response */
+ goto RESPONSE_FAILED;
+ }
+
+ Ustrcpy(big_buffer, US"end of data"); /* For error messages */
+
+ /* We now expect a response for every address that was accepted above,
+ in the same order. For those that get a response, their status is fixed;
+ any that are accepted have been handed over, even if later responses crash -
+ at least, that's how I read RFC 2033. */
+
+ for (address_item * addr = addrlist; addr; addr = addr->next)
+ {
+ if (addr->transport_return != PENDING_OK) continue;
+
+ if (lmtp_read_response(out, buffer, sizeof(buffer), '2', timeout))
+ {
+ addr->transport_return = OK;
+ if (LOGGING(smtp_confirmation))
+ {
+ const uschar *s = string_printing(buffer);
+ /* de-const safe here as string_printing known to have alloc'n'copied */
+ addr->message = (s == buffer)? US string_copy(s) : US s;
+ }
+ }
+ /* If the response has failed badly, use it for all the remaining pending
+ addresses and give up. */
+
+ else if (errno != 0 || buffer[0] == 0)
+ {
+ save_errno = errno;
+ check_response(&save_errno, addr->more_errno, buffer, &code,
+ &(addr->message));
+ addr->transport_return = (code == '5')? FAIL : DEFER;
+ for (address_item * a = addr->next; a; a = a->next)
+ {
+ if (a->transport_return != PENDING_OK) continue;
+ a->basic_errno = addr->basic_errno;
+ a->message = addr->message;
+ a->transport_return = addr->transport_return;
+ }
+ break;
+ }
+
+ /* Otherwise, it's an LMTP error code return for one address */
+
+ else
+ {
+ if (buffer[0] == '4')
+ {
+ addr->basic_errno = ERRNO_DATA4XX;
+ addr->more_errno |= ((buffer[1] - '0')*10 + buffer[2] - '0') << 8;
+ }
+ addr->message = string_sprintf("LMTP error after %s: %s", big_buffer,
+ string_printing(buffer));
+ addr->transport_return = (buffer[0] == '5')? FAIL : DEFER;
+ setflag(addr, af_pass_message); /* Allow message to go to user */
+ }
+ }
+ }
+
+/* The message transaction has completed successfully - this doesn't mean that
+all the addresses have necessarily been transferred, but each has its status
+set, so we change the yield to TRUE. */
+
+yield = TRUE;
+(void) lmtp_write_command(fd_in, "QUIT\r\n");
+(void) lmtp_read_response(out, buffer, sizeof(buffer), '2', 1);
+
+goto RETURN;
+
+
+/* Come here if any call to read_response, other than a response after the data
+phase, failed. Put the error in the top address - this will be replicated
+because the yield is still FALSE. (But omit ETIMEDOUT, as there will already be
+a suitable message.) Analyse the error, and if if isn't too bad, send a QUIT
+command. Wait for the response with a short timeout, so we don't wind up this
+process before the far end has had time to read the QUIT. */
+
+RESPONSE_FAILED:
+
+save_errno = errno;
+if (errno != ETIMEDOUT && errno != 0) addrlist->basic_errno = errno;
+addrlist->message = NULL;
+
+if (check_response(&save_errno, addrlist->more_errno,
+ buffer, &code, &(addrlist->message)))
+ {
+ (void) lmtp_write_command(fd_in, "QUIT\r\n");
+ (void) lmtp_read_response(out, buffer, sizeof(buffer), '2', 1);
+ }
+
+addrlist->transport_return = (code == '5')? FAIL : DEFER;
+if (code == '4' && save_errno > 0)
+ addrlist->message = string_sprintf("%s: %s", addrlist->message,
+ strerror(save_errno));
+goto KILL_AND_RETURN;
+
+/* Come here if there are errors during writing of a command or the message
+itself. This error will be applied to all the addresses. */
+
+WRITE_FAILED:
+
+addrlist->transport_return = PANIC;
+addrlist->basic_errno = errno;
+if (errno == ERRNO_CHHEADER_FAIL)
+ addrlist->message =
+ string_sprintf("Failed to expand headers_add or headers_remove: %s",
+ expand_string_message);
+else if (errno == ERRNO_FILTER_FAIL)
+ addrlist->message = US"Filter process failure";
+else if (errno == ERRNO_WRITEINCOMPLETE)
+ addrlist->message = US"Failed repeatedly to write data";
+else if (errno == ERRNO_SMTPFORMAT)
+ addrlist->message = US"overlong LMTP command generated";
+else
+ addrlist->message = string_sprintf("Error %d", errno);
+
+/* Come here after errors. Kill off the process. */
+
+KILL_AND_RETURN:
+
+if (pid > 0) killpg(pid, SIGKILL);
+
+/* Come here from all paths after the subprocess is created. Wait for the
+process, but with a timeout. */
+
+RETURN:
+
+(void)child_close(pid, timeout);
+
+if (fd_in >= 0) (void)close(fd_in);
+if (fd_out >= 0) (void)fclose(out);
+
+DEBUG(D_transport)
+ debug_printf("%s transport yields %d\n", tblock->name, yield);
+
+return yield;
+
+
+MINUS_N:
+ DEBUG(D_transport)
+ debug_printf("*** delivery by %s transport bypassed by -N option",
+ tblock->name);
+ addrlist->transport_return = OK;
+ return FALSE;
+}
+
+#endif /*!MACRO_PREDEF*/
+/* End of transport/lmtp.c */
diff --git a/src/transports/lmtp.h b/src/transports/lmtp.h
new file mode 100644
index 0000000..93f0f89
--- /dev/null
+++ b/src/transports/lmtp.h
@@ -0,0 +1,32 @@
+/*************************************************
+* 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 *cmd;
+ uschar *skt;
+ int timeout;
+ int options;
+ BOOL ignore_quota;
+} lmtp_transport_options_block;
+
+/* Data for reading the private options. */
+
+extern optionlist lmtp_transport_options[];
+extern int lmtp_transport_options_count;
+
+/* Block containing default values. */
+
+extern lmtp_transport_options_block lmtp_transport_option_defaults;
+
+/* The main and init entry points for the transport */
+
+extern BOOL lmtp_transport_entry(transport_instance *, address_item *);
+extern void lmtp_transport_init(transport_instance *);
+
+/* End of transports/lmtp.h */
diff --git a/src/transports/pipe.c b/src/transports/pipe.c
new file mode 100644
index 0000000..bdbe27d
--- /dev/null
+++ b/src/transports/pipe.c
@@ -0,0 +1,1124 @@
+/*************************************************
+* 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 "pipe.h"
+
+#ifdef HAVE_SETCLASSRESOURCES
+#include <login_cap.h>
+#endif
+
+
+
+/* Options specific to the pipe transport. They must be in alphabetic
+order (note that "_" comes before the lower case letters). Those starting
+with "*" are not settable by the user but are used by the option-reading
+software for alternative value types. Some options are stored in the transport
+instance block so as to be publicly visible; these are flagged with opt_public.
+*/
+#define LOFF(field) OPT_OFF(pipe_transport_options_block, field)
+
+optionlist pipe_transport_options[] = {
+ { "allow_commands", opt_stringptr, LOFF(allow_commands) },
+ { "batch_id", opt_stringptr | opt_public,
+ OPT_OFF(transport_instance, batch_id) },
+ { "batch_max", opt_int | opt_public,
+ OPT_OFF(transport_instance, batch_max) },
+ { "check_string", opt_stringptr, LOFF(check_string) },
+ { "command", opt_stringptr, LOFF(cmd) },
+ { "environment", opt_stringptr, LOFF(environment) },
+ { "escape_string", opt_stringptr, LOFF(escape_string) },
+ { "force_command", opt_bool, LOFF(force_command) },
+ { "freeze_exec_fail", opt_bool, LOFF(freeze_exec_fail) },
+ { "freeze_signal", opt_bool, LOFF(freeze_signal) },
+ { "ignore_status", opt_bool, LOFF(ignore_status) },
+ { "log_defer_output", opt_bool | opt_public,
+ OPT_OFF(transport_instance, log_defer_output) },
+ { "log_fail_output", opt_bool | opt_public,
+ OPT_OFF(transport_instance, log_fail_output) },
+ { "log_output", opt_bool | opt_public,
+ OPT_OFF(transport_instance, log_output) },
+ { "max_output", opt_mkint, LOFF(max_output) },
+ { "message_prefix", opt_stringptr, LOFF(message_prefix) },
+ { "message_suffix", opt_stringptr, LOFF(message_suffix) },
+ { "path", opt_stringptr, LOFF(path) },
+ { "permit_coredump", opt_bool, LOFF(permit_coredump) },
+ { "pipe_as_creator", opt_bool | opt_public,
+ OPT_OFF(transport_instance, deliver_as_creator) },
+ { "restrict_to_path", opt_bool, LOFF(restrict_to_path) },
+ { "return_fail_output",opt_bool | opt_public,
+ OPT_OFF(transport_instance, return_fail_output) },
+ { "return_output", opt_bool | opt_public,
+ OPT_OFF(transport_instance, return_output) },
+ { "temp_errors", opt_stringptr, LOFF(temp_errors) },
+ { "timeout", opt_time, LOFF(timeout) },
+ { "timeout_defer", opt_bool, LOFF(timeout_defer) },
+ { "umask", opt_octint, LOFF(umask) },
+ { "use_bsmtp", opt_bool, LOFF(use_bsmtp) },
+ #ifdef HAVE_SETCLASSRESOURCES
+ { "use_classresources", opt_bool, LOFF(use_classresources) },
+ #endif
+ { "use_crlf", opt_bool, LOFF(use_crlf) },
+ { "use_shell", opt_bool, LOFF(use_shell) },
+};
+
+/* Size of the options list. An extern variable has to be used so that its
+address can appear in the tables drtables.c. */
+
+int pipe_transport_options_count =
+ sizeof(pipe_transport_options)/sizeof(optionlist);
+
+
+#ifdef MACRO_PREDEF
+
+/* Dummy values */
+pipe_transport_options_block pipe_transport_option_defaults = {0};
+void pipe_transport_init(transport_instance *tblock) {}
+BOOL pipe_transport_entry(transport_instance *tblock, address_item *addr) {return FALSE;}
+
+#else /*!MACRO_PREDEF*/
+
+
+/* Default private options block for the pipe transport. */
+
+pipe_transport_options_block pipe_transport_option_defaults = {
+ .path = US"/bin:/usr/bin",
+ .temp_errors = US mac_expanded_string(EX_TEMPFAIL) ":"
+ mac_expanded_string(EX_CANTCREAT),
+ .umask = 022,
+ .max_output = 20480,
+ .timeout = 60*60,
+ /* all others null/zero/false */
+};
+
+
+
+/*************************************************
+* Setup entry point *
+*************************************************/
+
+/* Called for each delivery in the privileged state, just before the uid/gid
+are changed and the main entry point is called. In a system that supports the
+login_cap facilities, this function is used to set the class resource limits
+for the user. It may also re-enable coredumps.
+
+Arguments:
+ tblock points to the transport instance
+ addrlist addresses about to be delivered (not used)
+ dummy not used (doesn't pass back data)
+ uid the uid that will be set (not used)
+ gid the gid that will be set (not used)
+ errmsg where to put an error message
+
+Returns: OK, FAIL, or DEFER
+*/
+
+static int
+pipe_transport_setup(transport_instance *tblock, address_item *addrlist,
+ transport_feedback *dummy, uid_t uid, gid_t gid, uschar **errmsg)
+{
+pipe_transport_options_block *ob =
+ (pipe_transport_options_block *)(tblock->options_block);
+
+#ifdef HAVE_SETCLASSRESOURCES
+if (ob->use_classresources)
+ {
+ struct passwd *pw = getpwuid(uid);
+ if (pw != NULL)
+ {
+ login_cap_t *lc = login_getpwclass(pw);
+ if (lc != NULL)
+ {
+ setclassresources(lc);
+ login_close(lc);
+ }
+ }
+ }
+#endif
+
+#ifdef RLIMIT_CORE
+if (ob->permit_coredump)
+ {
+ struct rlimit rl;
+ rl.rlim_cur = RLIM_INFINITY;
+ rl.rlim_max = RLIM_INFINITY;
+ if (setrlimit(RLIMIT_CORE, &rl) < 0)
+ {
+#ifdef SETRLIMIT_NOT_SUPPORTED
+ if (errno != ENOSYS && errno != ENOTSUP)
+#endif
+ log_write(0, LOG_MAIN,
+ "delivery setrlimit(RLIMIT_CORE, RLIM_INFINITY) failed: %s",
+ strerror(errno));
+ }
+ }
+#endif
+
+return OK;
+}
+
+
+
+/*************************************************
+* 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
+pipe_transport_init(transport_instance *tblock)
+{
+pipe_transport_options_block *ob =
+ (pipe_transport_options_block *)(tblock->options_block);
+
+/* Set up the setup entry point, to be called in the privileged state */
+
+tblock->setup = pipe_transport_setup;
+
+/* If pipe_as_creator is set, then uid/gid should not be set. */
+
+if (tblock->deliver_as_creator && (tblock->uid_set || tblock->gid_set ||
+ tblock->expand_uid != NULL || tblock->expand_gid != NULL))
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG,
+ "both pipe_as_creator and an explicit uid/gid are set for the %s "
+ "transport", tblock->name);
+
+/* If a fixed uid field is set, then a gid field must also be set. */
+
+if (tblock->uid_set && !tblock->gid_set && tblock->expand_gid == NULL)
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG,
+ "user set without group for the %s transport", tblock->name);
+
+/* Temp_errors must consist only of digits and colons, but there can be
+spaces round the colons, so allow them too. */
+
+if (ob->temp_errors != NULL && Ustrcmp(ob->temp_errors, "*") != 0)
+ {
+ size_t p = Ustrspn(ob->temp_errors, "0123456789: ");
+ if (ob->temp_errors[p] != 0)
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG,
+ "temp_errors must be a list of numbers or an asterisk for the %s "
+ "transport", tblock->name);
+ }
+
+/* Only one of return_output/return_fail_output or log_output/log_fail_output
+should be set. */
+
+if (tblock->return_output && tblock->return_fail_output)
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG,
+ "both return_output and return_fail_output set for %s transport",
+ tblock->name);
+
+if (tblock->log_output && tblock->log_fail_output)
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG,
+ "both log_output and log_fail_output set for the %s transport",
+ tblock->name);
+
+/* If batch SMTP is set, force the check and escape strings, and arrange that
+headers are also escaped. */
+
+if (ob->use_bsmtp)
+ {
+ ob->check_string = US".";
+ ob->escape_string = US"..";
+ ob->options |= topt_escape_headers;
+ }
+
+/* If not batch SMTP, and message_prefix or message_suffix are unset, insert
+default values for them. */
+
+else
+ {
+ if (ob->message_prefix == NULL) ob->message_prefix =
+ US"From ${if def:return_path{$return_path}{MAILER-DAEMON}} ${tod_bsdinbox}\n";
+ if (ob->message_suffix == NULL) ob->message_suffix = US"\n";
+ }
+
+/* The restrict_to_path and use_shell options are incompatible */
+
+if (ob->restrict_to_path && ob->use_shell)
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG,
+ "both restrict_to_path and use_shell set for %s transport",
+ tblock->name);
+
+/* The allow_commands and use_shell options are incompatible */
+
+if (ob->allow_commands && ob->use_shell)
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG,
+ "both allow_commands and use_shell set for %s transport",
+ tblock->name);
+
+/* Set up the bitwise options for transport_write_message from the various
+driver options. Only one of body_only and headers_only can be set. */
+
+ob->options |=
+ (tblock->body_only ? topt_no_headers : 0)
+ | (tblock->headers_only ? topt_no_body : 0)
+ | (tblock->return_path_add ? topt_add_return_path : 0)
+ | (tblock->delivery_date_add ? topt_add_delivery_date : 0)
+ | (tblock->envelope_to_add ? topt_add_envelope_to : 0)
+ | (ob->use_crlf ? topt_use_crlf : 0);
+}
+
+
+
+/*************************************************
+* Set up direct (non-shell) command *
+*************************************************/
+
+/* This function is called when a command line is to be parsed by the transport
+and executed directly, without the use of /bin/sh.
+
+Arguments:
+ argvptr pointer to anchor for argv vector
+ cmd points to the command string
+ expand_arguments true if expansion is to occur
+ expand_fail error if expansion fails
+ addr chain of addresses
+ tname the transport name
+ ob the transport options block
+
+Returns: TRUE if all went well; otherwise an error will be
+ set in the first address and FALSE returned
+*/
+
+static BOOL
+set_up_direct_command(const uschar ***argvptr, uschar *cmd,
+ BOOL expand_arguments, int expand_fail, address_item *addr, uschar *tname,
+ pipe_transport_options_block *ob)
+{
+BOOL permitted = FALSE;
+const uschar **argv;
+
+/* Set up "transport <name>" to be put in any error messages, and then
+call the common function for creating an argument list and expanding
+the items if necessary. If it fails, this function fails (error information
+is in the addresses). */
+
+if (!transport_set_up_command(argvptr, cmd, expand_arguments, expand_fail,
+ addr, FALSE, string_sprintf("%.50s transport", tname), NULL))
+ return FALSE;
+
+/* Point to the set-up arguments. */
+
+argv = *argvptr;
+
+/* If allow_commands is set, see if the command is in the permitted list. */
+
+if (ob->allow_commands)
+ {
+ int sep = 0;
+ const uschar *s;
+ uschar *p;
+
+ if (!(s = expand_string(ob->allow_commands)))
+ {
+ addr->transport_return = DEFER;
+ addr->message = string_sprintf("failed to expand string \"%s\" "
+ "for %s transport: %s", ob->allow_commands, tname, expand_string_message);
+ return FALSE;
+ }
+
+ while ((p = string_nextinlist(&s, &sep, NULL, 0)))
+ if (Ustrcmp(p, argv[0]) == 0) { permitted = TRUE; break; }
+ }
+
+/* If permitted is TRUE it means the command was found in the allowed list, and
+no further checks are done. If permitted = FALSE, it either means
+allow_commands wasn't set, or that the command didn't match anything in the
+list. In both cases, if restrict_to_path is set, we fail if the command
+contains any slashes, but if restrict_to_path is not set, we must fail the
+command only if allow_commands is set. */
+
+if (!permitted)
+ {
+ if (ob->restrict_to_path)
+ {
+ if (Ustrchr(argv[0], '/') != NULL)
+ {
+ addr->transport_return = FAIL;
+ addr->message = string_sprintf("\"/\" found in \"%s\" (command for %s "
+ "transport) - failed for security reasons", cmd, tname);
+ return FALSE;
+ }
+ }
+
+ else if (ob->allow_commands)
+ {
+ addr->transport_return = FAIL;
+ addr->message = string_sprintf("\"%s\" command not permitted by %s "
+ "transport", argv[0], tname);
+ return FALSE;
+ }
+ }
+
+/* If the command is not an absolute path, search the PATH directories
+for it. */
+
+if (argv[0][0] != '/')
+ {
+ int sep = 0;
+ uschar *p;
+ const uschar *listptr = expand_string(ob->path);
+
+ while ((p = string_nextinlist(&listptr, &sep, NULL, 0)))
+ {
+ struct stat statbuf;
+ sprintf(CS big_buffer, "%.256s/%.256s", p, argv[0]);
+ if (Ustat(big_buffer, &statbuf) == 0)
+ {
+ argv[0] = string_copy(big_buffer);
+ break;
+ }
+ }
+ if (!p)
+ {
+ addr->transport_return = FAIL;
+ addr->message = string_sprintf("\"%s\" command not found for %s transport",
+ argv[0], tname);
+ return FALSE;
+ }
+ }
+
+return TRUE;
+}
+
+
+/*************************************************
+* Set up shell command *
+*************************************************/
+
+/* This function is called when a command line is to be passed to /bin/sh
+without parsing inside the transport.
+
+Arguments:
+ argvptr pointer to anchor for argv vector
+ cmd points to the command string
+ expand_arguments true if expansion is to occur
+ expand_fail error if expansion fails
+ addr chain of addresses
+ tname the transport name
+
+Returns: TRUE if all went well; otherwise an error will be
+ set in the first address and FALSE returned
+*/
+
+static BOOL
+set_up_shell_command(const uschar ***argvptr, uschar *cmd,
+ BOOL expand_arguments, int expand_fail, address_item *addr, uschar *tname)
+{
+const uschar **argv;
+
+*argvptr = argv = store_get((4)*sizeof(uschar *), GET_UNTAINTED);
+
+argv[0] = US"/bin/sh";
+argv[1] = US"-c";
+
+/* We have to take special action to handle the special "variable" called
+$pipe_addresses, which is not recognized by the normal expansion function. */
+
+if (expand_arguments)
+ {
+ uschar * p = Ustrstr(cmd, "pipe_addresses");
+ gstring * g = NULL;
+
+ DEBUG(D_transport)
+ debug_printf("shell pipe command before expansion:\n %s\n", cmd);
+
+ /* Allow $recipients in the expansion iff it comes from a system filter */
+
+ f.enable_dollar_recipients = addr && addr->parent &&
+ Ustrcmp(addr->parent->address, "system-filter") == 0;
+
+ if (p != NULL && (
+ (p > cmd && p[-1] == '$') ||
+ (p > cmd + 1 && p[-2] == '$' && p[-1] == '{' && p[14] == '}')))
+ {
+ uschar *q = p + 14;
+
+ if (p[-1] == '{') { q++; p--; }
+
+ g = string_get(Ustrlen(cmd) + 64);
+ g = string_catn(g, cmd, p - cmd - 1);
+
+ for (address_item * ad = addr; ad; ad = ad->next)
+ {
+ DEBUG(D_transport) if (is_tainted(ad->address))
+ debug_printf("tainted element '%s' from $pipe_addresses\n", ad->address);
+
+ /*XXX string_append_listele() ? */
+ if (ad != addr) g = string_catn(g, US" ", 1);
+ g = string_cat(g, ad->address);
+ }
+
+ g = string_cat(g, q);
+ argv[2] = (cmd = string_from_gstring(g)) ? expand_string(cmd) : NULL;
+ }
+ else
+ argv[2] = expand_string(cmd);
+
+ f.enable_dollar_recipients = FALSE;
+
+ if (!argv[2])
+ {
+ addr->transport_return = f.search_find_defer ? DEFER : expand_fail;
+ addr->message = string_sprintf("Expansion of command \"%s\" "
+ "in %s transport failed: %s",
+ cmd, tname, expand_string_message);
+ return FALSE;
+ }
+
+ DEBUG(D_transport)
+ debug_printf("shell pipe command after expansion:\n %s\n", argv[2]);
+ }
+else
+ {
+ DEBUG(D_transport)
+ debug_printf("shell pipe command (no expansion):\n %s\n", cmd);
+ argv[2] = cmd;
+ }
+
+argv[3] = US 0;
+return TRUE;
+}
+
+
+
+
+/*************************************************
+* Main entry point *
+*************************************************/
+
+/* See local README for interface details. This transport always returns FALSE,
+indicating that the status in the first address is the status for all addresses
+in a batch. */
+
+BOOL
+pipe_transport_entry(
+ transport_instance *tblock, /* data for this instantiation */
+ address_item *addr) /* address(es) we are working on */
+{
+pid_t pid, outpid;
+int fd_in, fd_out, rc;
+int envcount = 0;
+int envsep = 0;
+int expand_fail;
+pipe_transport_options_block *ob =
+ (pipe_transport_options_block *)(tblock->options_block);
+int timeout = ob->timeout;
+BOOL written_ok = FALSE;
+BOOL expand_arguments;
+const uschar **argv;
+uschar *envp[50];
+const uschar *envlist = ob->environment;
+uschar *cmd, *ss;
+uschar *eol = ob->use_crlf ? US"\r\n" : US"\n";
+transport_ctx tctx = {
+ .tblock = tblock,
+ .addr = addr,
+ .check_string = ob->check_string,
+ .escape_string = ob->escape_string,
+ ob->options | topt_not_socket /* set at initialization time */
+};
+
+DEBUG(D_transport) debug_printf("%s transport entered\n", tblock->name);
+
+/* Set up for the good case */
+
+addr->transport_return = OK;
+addr->basic_errno = 0;
+
+/* Pipes are not accepted as general addresses, but they can be generated from
+.forward files or alias files. In those cases, the pfr flag is set, and the
+command to be obeyed is pointed to by addr->local_part; it starts with the pipe
+symbol. In other cases, the command is supplied as one of the pipe transport's
+options. */
+
+if (testflag(addr, af_pfr) && addr->local_part[0] == '|')
+ if (ob->force_command)
+ {
+ /* Enables expansion of $address_pipe into separate arguments */
+ setflag(addr, af_force_command);
+ cmd = ob->cmd;
+ expand_arguments = TRUE;
+ expand_fail = PANIC;
+ }
+ else
+ {
+ cmd = addr->local_part + 1;
+ while (isspace(*cmd)) cmd++;
+ expand_arguments = testflag(addr, af_expand_pipe);
+ expand_fail = FAIL;
+ }
+else
+ {
+ cmd = ob->cmd;
+ expand_arguments = TRUE;
+ expand_fail = PANIC;
+ }
+
+/* If no command has been supplied, we are in trouble.
+We also check for an empty string since it may be
+coming from addr->local_part[0] == '|' */
+
+if (!cmd || !*cmd)
+ {
+ addr->transport_return = DEFER;
+ addr->message = string_sprintf("no command specified for %s transport",
+ tblock->name);
+ return FALSE;
+ }
+if (is_tainted(cmd))
+ {
+ DEBUG(D_transport) debug_printf("cmd '%s' is tainted\n", cmd);
+ addr->message = string_sprintf("Tainted '%s' (command "
+ "for %s transport) not permitted", cmd, tblock->name);
+ addr->transport_return = PANIC;
+ return FALSE;
+ }
+
+/* When a pipe is set up by a filter file, there may be values for $thisaddress
+and numerical the variables in existence. These are passed in
+addr->pipe_expandn for use here. */
+
+if (expand_arguments && addr->pipe_expandn)
+ {
+ uschar **ss = addr->pipe_expandn;
+ expand_nmax = -1;
+ if (*ss) filter_thisaddress = *ss++;
+ while (*ss)
+ {
+ expand_nstring[++expand_nmax] = *ss;
+ expand_nlength[expand_nmax] = Ustrlen(*ss++);
+ }
+ }
+
+/* The default way of processing the command is to split it up into arguments
+here, and run it directly. This offers some security advantages. However, there
+are installations that want by default to run commands under /bin/sh always, so
+there is an option to do that. */
+
+if (ob->use_shell)
+ {
+ if (!set_up_shell_command(&argv, cmd, expand_arguments, expand_fail, addr,
+ tblock->name)) return FALSE;
+ }
+else if (!set_up_direct_command(&argv, cmd, expand_arguments, expand_fail, addr,
+ tblock->name, ob)) return FALSE;
+
+expand_nmax = -1; /* Reset */
+filter_thisaddress = NULL;
+
+/* Set up the environment for the command. */
+
+envp[envcount++] = string_sprintf("LOCAL_PART=%s", deliver_localpart);
+envp[envcount++] = string_sprintf("LOGNAME=%s", deliver_localpart);
+envp[envcount++] = string_sprintf("USER=%s", deliver_localpart);
+envp[envcount++] = string_sprintf("LOCAL_PART_PREFIX=%#s",
+ deliver_localpart_prefix);
+envp[envcount++] = string_sprintf("LOCAL_PART_SUFFIX=%#s",
+ deliver_localpart_suffix);
+envp[envcount++] = string_sprintf("DOMAIN=%s", deliver_domain);
+envp[envcount++] = string_sprintf("HOME=%#s", deliver_home);
+envp[envcount++] = string_sprintf("MESSAGE_ID=%s", message_id);
+envp[envcount++] = string_sprintf("PATH=%s", expand_string(ob->path));
+envp[envcount++] = string_sprintf("RECIPIENT=%#s%#s%#s@%#s",
+ deliver_localpart_prefix, deliver_localpart, deliver_localpart_suffix,
+ deliver_domain);
+envp[envcount++] = string_sprintf("QUALIFY_DOMAIN=%s", qualify_domain_sender);
+envp[envcount++] = string_sprintf("SENDER=%s", sender_address);
+envp[envcount++] = US"SHELL=/bin/sh";
+
+if (addr->host_list)
+ envp[envcount++] = string_sprintf("HOST=%s", addr->host_list->name);
+
+if (f.timestamps_utc)
+ envp[envcount++] = US"TZ=UTC";
+else if (timezone_string && timezone_string[0])
+ envp[envcount++] = string_sprintf("TZ=%s", timezone_string);
+
+/* Add any requested items */
+
+if (envlist)
+ if (!(envlist = expand_cstring(envlist)))
+ {
+ addr->transport_return = DEFER;
+ addr->message = string_sprintf("failed to expand string \"%s\" "
+ "for %s transport: %s", ob->environment, tblock->name,
+ expand_string_message);
+ return FALSE;
+ }
+
+while ((ss = string_nextinlist(&envlist, &envsep, NULL, 0)))
+ {
+ if (envcount > nelem(envp) - 2)
+ {
+ addr->transport_return = DEFER;
+ addr->basic_errno = E2BIG;
+ addr->message = string_sprintf("too many environment settings for "
+ "%s transport", tblock->name);
+ return FALSE;
+ }
+ envp[envcount++] = string_copy(ss);
+ }
+
+envp[envcount] = NULL;
+
+/* If the -N option is set, can't do any more. */
+
+if (f.dont_deliver)
+ {
+ DEBUG(D_transport)
+ debug_printf("*** delivery by %s transport bypassed by -N option",
+ tblock->name);
+ return FALSE;
+ }
+
+
+/* Handling the output from the pipe is tricky. If a file for catching this
+output is provided, we could in theory just hand that fd over to the process,
+but this isn't very safe because it might loop and carry on writing for
+ever (which is exactly what happened in early versions of Exim). Therefore we
+use the standard child_open() function, which creates pipes. We can then read
+our end of the output pipe and count the number of bytes that come through,
+chopping the sub-process if it exceeds some limit.
+
+However, this means we want to run a sub-process with both its input and output
+attached to pipes. We can't handle that easily from a single parent process
+using straightforward code such as the transport_write_message() function
+because the subprocess might not be reading its input because it is trying to
+write to a full output pipe. The complication of redesigning the world to
+handle this is too great - simpler just to run another process to do the
+reading of the output pipe. */
+
+
+/* As this is a local transport, we are already running with the required
+uid/gid and current directory. Request that the new process be a process group
+leader, so we can kill it and all its children on a timeout. */
+
+if ((pid = child_open(USS argv, envp, ob->umask, &fd_in, &fd_out, TRUE,
+ US"pipe-tpt-cmd")) < 0)
+ {
+ addr->transport_return = DEFER;
+ addr->message = string_sprintf(
+ "Failed to create child process for %s transport: %s", tblock->name,
+ strerror(errno));
+ return FALSE;
+ }
+tctx.u.fd = fd_in;
+
+/* Now fork a process to handle the output that comes down the pipe. */
+
+if ((outpid = exim_fork(US"pipe-tpt-output")) < 0)
+ {
+ addr->basic_errno = errno;
+ addr->transport_return = DEFER;
+ addr->message = string_sprintf(
+ "Failed to create process for handling output in %s transport",
+ tblock->name);
+ (void)close(fd_in);
+ (void)close(fd_out);
+ return FALSE;
+ }
+
+/* This is the code for the output-handling subprocess. Read from the pipe
+in chunks, and write to the return file if one is provided. Keep track of
+the number of bytes handled. If the limit is exceeded, try to kill the
+subprocess group, and in any case close the pipe and exit, which should cause
+the subprocess to fail. */
+
+if (outpid == 0)
+ {
+ int count = 0;
+ (void)close(fd_in);
+ set_process_info("reading output from |%s", cmd);
+ while ((rc = read(fd_out, big_buffer, big_buffer_size)) > 0)
+ {
+ if (addr->return_file >= 0)
+ if(write(addr->return_file, big_buffer, rc) != rc)
+ DEBUG(D_transport) debug_printf("Problem writing to return_file\n");
+ count += rc;
+ if (count > ob->max_output)
+ {
+ DEBUG(D_transport) debug_printf("Too much output from pipe - killed\n");
+ if (addr->return_file >= 0)
+ {
+ uschar *message = US"\n\n*** Too much output - remainder discarded ***\n";
+ rc = Ustrlen(message);
+ if(write(addr->return_file, message, rc) != rc)
+ DEBUG(D_transport) debug_printf("Problem writing to return_file\n");
+ }
+ killpg(pid, SIGKILL);
+ break;
+ }
+ }
+ (void)close(fd_out);
+ _exit(0);
+ }
+
+(void)close(fd_out); /* Not used in this process */
+
+
+/* Carrying on now with the main parent process. Attempt to write the message
+to it down the pipe. It is a fallacy to think that you can detect write errors
+when the sub-process fails to read the pipe. The parent process may complete
+writing and close the pipe before the sub-process completes. We could sleep a
+bit here to let the sub-process get going, but it may still not complete. So we
+ignore all writing errors. (When in the test harness, we do do a short sleep so
+any debugging output is likely to be in the same order.) */
+
+testharness_pause_ms(500);
+
+DEBUG(D_transport) debug_printf("Writing message to pipe\n");
+
+/* Arrange to time out writes if there is a timeout set. */
+
+if (timeout > 0)
+ {
+ sigalrm_seen = FALSE;
+ transport_write_timeout = timeout;
+ }
+
+/* Reset the counter of bytes written */
+
+transport_count = 0;
+
+/* First write any configured prefix information */
+
+if (ob->message_prefix)
+ {
+ uschar *prefix = expand_string(ob->message_prefix);
+ if (!prefix)
+ {
+ addr->transport_return = f.search_find_defer? DEFER : PANIC;
+ addr->message = string_sprintf("Expansion of \"%s\" (prefix for %s "
+ "transport) failed: %s", ob->message_prefix, tblock->name,
+ expand_string_message);
+ return FALSE;
+ }
+ if (!transport_write_block(&tctx, prefix, Ustrlen(prefix), FALSE))
+ goto END_WRITE;
+ }
+
+/* If the use_bsmtp option is set, we need to write SMTP prefix information.
+The various different values for batching are handled outside; if there is more
+than one address available here, all must be included. Force SMTP dot-handling.
+*/
+
+if (ob->use_bsmtp)
+ {
+ if (!transport_write_string(fd_in, "MAIL FROM:<%s>%s", return_path, eol))
+ goto END_WRITE;
+
+ for (address_item * a = addr; a; a = a->next)
+ if (!transport_write_string(fd_in,
+ "RCPT TO:<%s>%s",
+ transport_rcpt_address(a, tblock->rcpt_include_affixes),
+ eol))
+ goto END_WRITE;
+
+ if (!transport_write_string(fd_in, "DATA%s", eol)) goto END_WRITE;
+ }
+
+/* Now the actual message */
+
+if (!transport_write_message(&tctx, 0))
+ goto END_WRITE;
+
+/* Now any configured suffix */
+
+if (ob->message_suffix)
+ {
+ uschar *suffix = expand_string(ob->message_suffix);
+ if (!suffix)
+ {
+ addr->transport_return = f.search_find_defer? DEFER : PANIC;
+ addr->message = string_sprintf("Expansion of \"%s\" (suffix for %s "
+ "transport) failed: %s", ob->message_suffix, tblock->name,
+ expand_string_message);
+ return FALSE;
+ }
+ if (!transport_write_block(&tctx, suffix, Ustrlen(suffix), FALSE))
+ goto END_WRITE;
+ }
+
+/* If local_smtp, write the terminating dot. */
+
+if (ob->use_bsmtp && !transport_write_string(fd_in, ".%s", eol))
+ goto END_WRITE;
+
+/* Flag all writing completed successfully. */
+
+written_ok = TRUE;
+
+/* Come here if there are errors during writing. */
+
+END_WRITE:
+
+/* OK, the writing is now all done. Close the pipe. */
+
+(void) close(fd_in);
+
+/* Handle errors during writing. For timeouts, set the timeout for waiting for
+the child process to 1 second. If the process at the far end of the pipe died
+without reading all of it, we expect an EPIPE error, which should be ignored.
+We used also to ignore WRITEINCOMPLETE but the writing function is now cleverer
+at handling OS where the death of a pipe doesn't give EPIPE immediately. See
+comments therein. */
+
+if (!written_ok)
+ {
+ if (errno == ETIMEDOUT)
+ {
+ addr->message = string_sprintf("%stimeout while writing to pipe",
+ f.transport_filter_timed_out ? "transport filter " : "");
+ addr->transport_return = ob->timeout_defer? DEFER : FAIL;
+ timeout = 1;
+ }
+ else if (errno == EPIPE)
+ {
+ debug_printf("transport error EPIPE ignored\n");
+ }
+ else
+ {
+ addr->transport_return = PANIC;
+ addr->basic_errno = errno;
+ if (errno == ERRNO_CHHEADER_FAIL)
+ addr->message =
+ string_sprintf("Failed to expand headers_add or headers_remove: %s",
+ expand_string_message);
+ else if (errno == ERRNO_FILTER_FAIL)
+ addr->message = string_sprintf("Transport filter process failed (%d)%s",
+ addr->more_errno,
+ (addr->more_errno == EX_EXECFAILED)? ": unable to execute command" : "");
+ else if (errno == ERRNO_WRITEINCOMPLETE)
+ addr->message = US"Failed repeatedly to write data";
+ else
+ addr->message = string_sprintf("Error %d", errno);
+ return FALSE;
+ }
+ }
+
+/* Wait for the child process to complete and take action if the returned
+status is nonzero. The timeout will be just 1 second if any of the writes
+above timed out. */
+
+if ((rc = child_close(pid, timeout)) != 0)
+ {
+ uschar * tmsg = addr->message
+ ? string_sprintf(" (preceded by %s)", addr->message) : US"";
+
+ /* The process did not complete in time; kill its process group and fail
+ the delivery. It appears to be necessary to kill the output process too, as
+ otherwise it hangs on for some time if the actual pipe process is sleeping.
+ (At least, that's what I observed on Solaris 2.5.1.) Since we are failing
+ the delivery, that shouldn't cause any problem. */
+
+ if (rc == -256)
+ {
+ killpg(pid, SIGKILL);
+ kill(outpid, SIGKILL);
+ addr->transport_return = ob->timeout_defer? DEFER : FAIL;
+ addr->message = string_sprintf("pipe delivery process timed out%s", tmsg);
+ }
+
+ /* Wait() failed. */
+
+ else if (rc == -257)
+ {
+ addr->transport_return = PANIC;
+ addr->message = string_sprintf("Wait() failed for child process of %s "
+ "transport: %s%s", tblock->name, strerror(errno), tmsg);
+ }
+
+ /* Since the transport_filter timed out we assume it has sent the child process
+ a malformed or incomplete data stream. Kill off the child process
+ and prevent checking its exit status as it will has probably exited in error.
+ This prevents the transport_filter timeout message from getting overwritten
+ by the exit error which is not the cause of the problem. */
+
+ else if (f.transport_filter_timed_out)
+ {
+ killpg(pid, SIGKILL);
+ kill(outpid, SIGKILL);
+ }
+
+ /* Either the process completed, but yielded a non-zero (necessarily
+ positive) status, or the process was terminated by a signal (rc will contain
+ the negation of the signal number). Treat killing by signal as failure unless
+ status is being ignored. By default, the message is bounced back, unless
+ freeze_signal is set, in which case it is frozen instead. */
+
+ else if (rc < 0)
+ {
+ if (ob->freeze_signal)
+ {
+ addr->transport_return = DEFER;
+ addr->special_action = SPECIAL_FREEZE;
+ addr->message = string_sprintf("Child process of %s transport (running "
+ "command \"%s\") was terminated by signal %d (%s)%s", tblock->name, cmd,
+ -rc, os_strsignal(-rc), tmsg);
+ }
+ else if (!ob->ignore_status)
+ {
+ addr->transport_return = FAIL;
+ addr->message = string_sprintf("Child process of %s transport (running "
+ "command \"%s\") was terminated by signal %d (%s)%s", tblock->name, cmd,
+ -rc, os_strsignal(-rc), tmsg);
+ }
+ }
+
+ /* For positive values (process terminated with non-zero status), we need a
+ status code to request deferral. A number of systems contain the following
+ line in sysexits.h:
+
+ #define EX_TEMPFAIL 75
+
+ with the description
+
+ EX_TEMPFAIL -- temporary failure, indicating something that
+ is not really an error. In sendmail, this means
+ that a mailer (e.g.) could not create a connection,
+ and the request should be reattempted later.
+
+ Based on this, we use exit code EX_TEMPFAIL as a default to mean "defer" when
+ not ignoring the returned status. However, there is now an option that
+ contains a list of temporary codes, with TEMPFAIL and CANTCREAT as defaults.
+
+ Another case that needs special treatment is if execve() failed (typically
+ the command that was given is a non-existent path). By default this is
+ treated as just another failure, but if freeze_exec_fail is set, the reaction
+ is to freeze the message rather than bounce the address. Exim used to signal
+ this failure with EX_UNAVAILABLE, which is defined in many systems as
+
+ #define EX_UNAVAILABLE 69
+
+ with the description
+
+ EX_UNAVAILABLE -- A service is unavailable. This can occur
+ if a support program or file does not exist. This
+ can also be used as a catchall message when something
+ you wanted to do doesn't work, but you don't know why.
+
+ However, this can be confused with a command that actually returns 69 because
+ something *it* wanted is unavailable. At release 4.21, Exim was changed to
+ use return code 127 instead, because this is what the shell returns when it
+ is unable to exec a command. We define it as EX_EXECFAILED, and use it in
+ child.c to signal execve() failure and other unexpected failures such as
+ setuid() not working - though that won't be the case here because we aren't
+ changing uid. */
+
+ else
+ {
+ /* Always handle execve() failure specially if requested to */
+
+ if (ob->freeze_exec_fail && rc == EX_EXECFAILED)
+ {
+ addr->transport_return = DEFER;
+ addr->special_action = SPECIAL_FREEZE;
+ addr->message = string_sprintf("pipe process failed to exec \"%s\"%s",
+ cmd, tmsg);
+ }
+
+ /* Otherwise take action only if not ignoring status */
+
+ else if (!ob->ignore_status)
+ {
+ uschar *ss;
+ gstring * g;
+
+ /* If temp_errors is "*" all codes are temporary. Initialization checks
+ that it's either "*" or a list of numbers. If not "*", scan the list of
+ temporary failure codes; if any match, the result is DEFER. */
+
+ if (ob->temp_errors[0] == '*')
+ addr->transport_return = DEFER;
+
+ else
+ {
+ const uschar *s = ob->temp_errors;
+ uschar *p;
+ int sep = 0;
+
+ addr->transport_return = FAIL;
+ while ((p = string_nextinlist(&s,&sep,NULL,0)))
+ if (rc == Uatoi(p)) { addr->transport_return = DEFER; break; }
+ }
+
+ /* Ensure the message contains the expanded command and arguments. This
+ doesn't have to be brilliantly efficient - it is an error situation. */
+
+ addr->message = string_sprintf("Child process of %s transport returned "
+ "%d", tblock->name, rc);
+ g = string_cat(NULL, addr->message);
+
+ /* If the return code is > 128, it often means that a shell command
+ was terminated by a signal. */
+
+ ss = (rc > 128)?
+ string_sprintf("(could mean shell command ended by signal %d (%s))",
+ rc-128, os_strsignal(rc-128)) :
+ US os_strexit(rc);
+
+ if (*ss)
+ {
+ g = string_catn(g, US" ", 1);
+ g = string_cat (g, ss);
+ }
+
+ /* Now add the command and arguments */
+
+ g = string_catn(g, US" from command:", 14);
+
+ for (int i = 0; i < sizeof(argv)/sizeof(int *) && argv[i] != NULL; i++)
+ {
+ BOOL quote = FALSE;
+ g = string_catn(g, US" ", 1);
+ if (Ustrpbrk(argv[i], " \t") != NULL)
+ {
+ quote = TRUE;
+ g = string_catn(g, US"\"", 1);
+ }
+ g = string_cat(g, argv[i]);
+ if (quote)
+ g = string_catn(g, US"\"", 1);
+ }
+
+ /* Add previous filter timeout message, if present. */
+
+ if (*tmsg)
+ g = string_cat(g, tmsg);
+
+ addr->message = string_from_gstring(g);
+ }
+ }
+ }
+
+/* Ensure all subprocesses (in particular, the output handling process)
+are complete before we pass this point. */
+
+while (wait(&rc) >= 0);
+
+DEBUG(D_transport) debug_printf("%s transport yielded %d\n", tblock->name,
+ addr->transport_return);
+
+/* If there has been a problem, the message in addr->message contains details
+of the pipe command. We don't want to expose these to the world, so we set up
+something bland to return to the sender. */
+
+if (addr->transport_return != OK)
+ addr->user_message = US"local delivery failed";
+
+return FALSE;
+}
+
+#endif /*!MACRO_PREDEF*/
+/* End of transport/pipe.c */
diff --git a/src/transports/pipe.h b/src/transports/pipe.h
new file mode 100644
index 0000000..ed5c142
--- /dev/null
+++ b/src/transports/pipe.h
@@ -0,0 +1,51 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) University of Cambridge 1995 - 2014 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+/* Private structure for the private options. */
+
+typedef struct {
+ uschar *cmd;
+ uschar *allow_commands;
+ uschar *environment;
+ uschar *path;
+ uschar *message_prefix;
+ uschar *message_suffix;
+ uschar *temp_errors;
+ uschar *check_string;
+ uschar *escape_string;
+ int umask;
+ int max_output;
+ int timeout;
+ int options;
+ BOOL force_command;
+ BOOL freeze_exec_fail;
+ BOOL freeze_signal;
+ BOOL ignore_status;
+ BOOL permit_coredump;
+ BOOL restrict_to_path;
+ BOOL timeout_defer;
+ BOOL use_shell;
+ BOOL use_bsmtp;
+ BOOL use_classresources;
+ BOOL use_crlf;
+} pipe_transport_options_block;
+
+/* Data for reading the private options. */
+
+extern optionlist pipe_transport_options[];
+extern int pipe_transport_options_count;
+
+/* Block containing default values. */
+
+extern pipe_transport_options_block pipe_transport_option_defaults;
+
+/* The main and init entry points for the transport */
+
+extern BOOL pipe_transport_entry(transport_instance *, address_item *);
+extern void pipe_transport_init(transport_instance *);
+
+/* End of transports/pipe.h */
diff --git a/src/transports/queuefile.c b/src/transports/queuefile.c
new file mode 100644
index 0000000..74131cc
--- /dev/null
+++ b/src/transports/queuefile.c
@@ -0,0 +1,286 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) Andrew Colin Kissa <andrew@topdog.za.net> 2016 */
+/* Copyright (c) University of Cambridge 2016 */
+/* Copyright (c) The Exim Maintainers 1995 - 2021 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+
+
+#include "../exim.h"
+
+#ifdef EXPERIMENTAL_QUEUEFILE /* whole file */
+#include "queuefile.h"
+
+#ifndef EXIM_HAVE_OPENAT
+# error queuefile transport reqires openat() support
+#endif
+
+/* Options specific to the appendfile transport. They must be in alphabetic
+order (note that "_" comes before the lower case letters). Some of them are
+stored in the publicly visible instance block - these are flagged with the
+opt_public flag. */
+
+optionlist queuefile_transport_options[] = {
+ { "directory", opt_stringptr,
+ OPT_OFF(queuefile_transport_options_block, dirname) },
+};
+
+
+/* Size of the options list. An extern variable has to be used so that its
+address can appear in the tables drtables.c. */
+
+int queuefile_transport_options_count =
+ sizeof(queuefile_transport_options) / sizeof(optionlist);
+
+
+#ifdef MACRO_PREDEF
+
+/* Dummy values */
+queuefile_transport_options_block queuefile_transport_option_defaults = {0};
+void queuefile_transport_init(transport_instance *tblock) {}
+BOOL queuefile_transport_entry(transport_instance *tblock, address_item *addr) {return FALSE;}
+
+#else /*!MACRO_PREDEF*/
+
+
+
+/* Default private options block for the appendfile transport. */
+
+queuefile_transport_options_block queuefile_transport_option_defaults = {
+ NULL, /* dirname */
+};
+
+/*************************************************
+* Initialization entry point *
+*************************************************/
+
+void queuefile_transport_init(transport_instance *tblock)
+{
+queuefile_transport_options_block *ob =
+ (queuefile_transport_options_block *) tblock->options_block;
+
+if (!ob->dirname)
+ log_write(0, LOG_PANIC_DIE | LOG_CONFIG,
+ "directory must be set for the %s transport", tblock->name);
+}
+
+/* This function will copy from a file to another
+
+Arguments:
+ dst fd to write to (the destination queue file)
+ src fd to read from (the spool queue file)
+
+Returns: TRUE if all went well, FALSE otherwise with errno set
+*/
+
+static BOOL
+copy_spool_file(int dst, int src)
+{
+int i, j;
+uschar buffer[16384];
+
+if (lseek(src, 0, SEEK_SET) != 0)
+ return FALSE;
+
+do
+ if ((j = read(src, buffer, sizeof(buffer))) > 0)
+ for (uschar * s = buffer; (i = write(dst, s, j)) != j; s += i, j -= i)
+ if (i < 0)
+ return FALSE;
+ else if (j < 0)
+ return FALSE;
+while (j > 0);
+return TRUE;
+}
+
+/* This function performs the actual copying of the header
+and data files to the destination directory
+
+Arguments:
+ tb the transport block
+ addr address_item being processed
+ dstpath destination directory name
+ sdfd int Source directory fd
+ ddfd int Destination directory fd
+ link_file BOOL use linkat instead of data copy
+ srcfd fd for data file, or -1 for header file
+
+Returns: TRUE if all went well, FALSE otherwise
+*/
+
+static BOOL
+copy_spool_files(transport_instance * tb, address_item * addr,
+ const uschar * dstpath, int sdfd, int ddfd, BOOL link_file, int srcfd)
+{
+BOOL is_hdr_file = srcfd < 0;
+const uschar * suffix = srcfd < 0 ? US"H" : US"D";
+int dstfd;
+const uschar * filename = string_sprintf("%s-%s", message_id, suffix);
+const uschar * srcpath = spool_fname(US"input", message_subdir, message_id, suffix);
+const uschar * s, * op;
+
+dstpath = string_sprintf("%s/%s-%s", dstpath, message_id, suffix);
+
+if (link_file)
+ {
+ DEBUG(D_transport) debug_printf("%s transport, linking %s => %s\n",
+ tb->name, srcpath, dstpath);
+
+ if (linkat(sdfd, CCS filename, ddfd, CCS filename, 0) >= 0)
+ return TRUE;
+
+ op = US"linking";
+ s = dstpath;
+ }
+else /* use data copy */
+ {
+ DEBUG(D_transport) debug_printf("%s transport, copying %s => %s\n",
+ tb->name, srcpath, dstpath);
+
+ if ( (s = dstpath,
+ (dstfd = exim_openat4(ddfd, CCS filename, O_RDWR|O_CREAT|O_EXCL, SPOOL_MODE))
+ < 0
+ )
+ || is_hdr_file
+ && (s = srcpath, (srcfd = exim_openat(sdfd, CCS filename, O_RDONLY)) < 0)
+ )
+ op = US"opening";
+
+ else
+ if (s = dstpath, fchmod(dstfd, SPOOL_MODE) != 0)
+ op = US"setting perms on";
+ else
+ if (!copy_spool_file(dstfd, srcfd))
+ op = US"creating";
+ else
+ return TRUE;
+ }
+
+addr->basic_errno = errno;
+addr->message = string_sprintf("%s transport %s file: %s failed with error: %s",
+ tb->name, op, s, strerror(errno));
+addr->transport_return = DEFER;
+return FALSE;
+}
+
+/*************************************************
+* Main entry point *
+*************************************************/
+
+/* This transport always returns FALSE, indicating that the status in
+the first address is the status for all addresses in a batch. */
+
+BOOL
+queuefile_transport_entry(transport_instance * tblock, address_item * addr)
+{
+queuefile_transport_options_block * ob =
+ (queuefile_transport_options_block *) tblock->options_block;
+BOOL can_link;
+uschar * sourcedir = spool_dname(US"input", message_subdir);
+uschar * s, * dstdir;
+struct stat dstatbuf, sstatbuf;
+int ddfd = -1, sdfd = -1;
+
+DEBUG(D_transport)
+ debug_printf("%s transport entered\n", tblock->name);
+
+#ifndef O_DIRECTORY
+# define O_DIRECTORY 0
+#endif
+#ifndef O_NOFOLLOW
+# define O_NOFOLLOW 0
+#endif
+
+if (!(dstdir = expand_string(ob->dirname)))
+ {
+ addr->message = string_sprintf("%s transport: failed to expand dirname option",
+ tblock->name);
+ addr->transport_return = DEFER;
+ return FALSE;
+ }
+if (*dstdir != '/')
+ {
+ addr->transport_return = PANIC;
+ addr->message = string_sprintf("%s transport directory: "
+ "%s is not absolute", tblock->name, dstdir);
+ return FALSE;
+ }
+
+/* Open the source and destination directories and check if they are
+on the same filesystem, so we can hard-link files rather than copying. */
+
+if ( (s = dstdir,
+ (ddfd = Uopen(s, O_RDONLY | O_DIRECTORY | O_NOFOLLOW, 0)) < 0)
+ || (s = sourcedir,
+ (sdfd = Uopen(sourcedir, O_RDONLY | O_DIRECTORY | O_NOFOLLOW, 0)) < 0)
+ )
+ {
+ addr->transport_return = PANIC;
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("%s transport accessing directory: %s "
+ "failed with error: %s", tblock->name, s, strerror(errno));
+ if (ddfd >= 0) (void) close(ddfd);
+ return FALSE;
+ }
+
+if ( (s = dstdir, fstat(ddfd, &dstatbuf) < 0)
+ || (s = sourcedir, fstat(sdfd, &sstatbuf) < 0)
+ )
+ {
+ addr->transport_return = PANIC;
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("%s transport fstat on directory fd: "
+ "%s failed with error: %s", tblock->name, s, strerror(errno));
+ goto RETURN;
+ }
+can_link = (dstatbuf.st_dev == sstatbuf.st_dev);
+
+if (f.dont_deliver)
+ {
+ DEBUG(D_transport)
+ debug_printf("*** delivery by %s transport bypassed by -N option\n",
+ tblock->name);
+ addr->transport_return = OK;
+ goto RETURN;
+ }
+
+/* Link or copy the header and data spool files */
+
+DEBUG(D_transport)
+ debug_printf("%s transport, copying header file\n", tblock->name);
+
+if (!copy_spool_files(tblock, addr, dstdir, sdfd, ddfd, can_link, -1))
+ goto RETURN;
+
+DEBUG(D_transport)
+ debug_printf("%s transport, copying data file\n", tblock->name);
+
+if (!copy_spool_files(tblock, addr, dstdir, sdfd, ddfd, can_link,
+ deliver_datafile))
+ {
+ DEBUG(D_transport)
+ debug_printf("%s transport, copying data file failed, "
+ "unlinking the header file\n", tblock->name);
+ Uunlink(string_sprintf("%s/%s-H", dstdir, message_id));
+ goto RETURN;
+ }
+
+DEBUG(D_transport)
+ debug_printf("%s transport succeeded\n", tblock->name);
+
+addr->transport_return = OK;
+
+RETURN:
+if (ddfd >= 0) (void) close(ddfd);
+if (sdfd >= 0) (void) close(sdfd);
+
+/* A return of FALSE means that if there was an error, a common error was
+put in the first address of a batch. */
+return FALSE;
+}
+
+#endif /*!MACRO_PREDEF*/
+#endif /*EXPERIMENTAL_QUEUEFILE*/
diff --git a/src/transports/queuefile.h b/src/transports/queuefile.h
new file mode 100644
index 0000000..0e45b51
--- /dev/null
+++ b/src/transports/queuefile.h
@@ -0,0 +1,29 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) Andrew Colin Kissa <andrew@topdog.za.net> 2016 */
+/* Copyright (c) University of Cambridge 2016 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+/* Private structure for the private options. */
+
+typedef struct {
+ uschar *dirname;
+} queuefile_transport_options_block;
+
+/* Data for reading the private options. */
+
+extern optionlist queuefile_transport_options[];
+extern int queuefile_transport_options_count;
+
+/* Block containing default values. */
+
+extern queuefile_transport_options_block queuefile_transport_option_defaults;
+
+/* The main and init entry points for the transport */
+
+extern BOOL queuefile_transport_entry(transport_instance *, address_item *);
+extern void queuefile_transport_init(transport_instance *);
+
+/* End of transports/queuefile.h */
diff --git a/src/transports/smtp.c b/src/transports/smtp.c
new file mode 100644
index 0000000..7f529b7
--- /dev/null
+++ b/src/transports/smtp.c
@@ -0,0 +1,6071 @@
+/*************************************************
+* 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 "smtp.h"
+
+#if defined(SUPPORT_DANE) && defined(DISABLE_TLS)
+# error TLS is required for DANE
+#endif
+
+
+/* Options specific to the smtp transport. This transport also supports LMTP
+over TCP/IP. The options must be in alphabetic order (note that "_" comes
+before the lower case letters). Some live in the transport_instance block so as
+to be publicly visible; these are flagged with opt_public. */
+
+#define LOFF(field) OPT_OFF(smtp_transport_options_block, field)
+
+optionlist smtp_transport_options[] = {
+ { "*expand_multi_domain", opt_stringptr | opt_hidden | opt_public,
+ OPT_OFF(transport_instance, expand_multi_domain) },
+ { "*expand_retry_include_ip_address", opt_stringptr | opt_hidden,
+ LOFF(expand_retry_include_ip_address) },
+
+ { "address_retry_include_sender", opt_bool,
+ LOFF(address_retry_include_sender) },
+ { "allow_localhost", opt_bool, LOFF(allow_localhost) },
+#ifdef EXPERIMENTAL_ARC
+ { "arc_sign", opt_stringptr, LOFF(arc_sign) },
+#endif
+ { "authenticated_sender", opt_stringptr, LOFF(authenticated_sender) },
+ { "authenticated_sender_force", opt_bool, LOFF(authenticated_sender_force) },
+ { "command_timeout", opt_time, LOFF(command_timeout) },
+ { "connect_timeout", opt_time, LOFF(connect_timeout) },
+ { "connection_max_messages", opt_int | opt_public,
+ OPT_OFF(transport_instance, connection_max_messages) },
+# ifdef SUPPORT_DANE
+ { "dane_require_tls_ciphers", opt_stringptr, LOFF(dane_require_tls_ciphers) },
+# endif
+ { "data_timeout", opt_time, LOFF(data_timeout) },
+ { "delay_after_cutoff", opt_bool, LOFF(delay_after_cutoff) },
+#ifndef DISABLE_DKIM
+ { "dkim_canon", opt_stringptr, LOFF(dkim.dkim_canon) },
+ { "dkim_domain", opt_stringptr, LOFF(dkim.dkim_domain) },
+ { "dkim_hash", opt_stringptr, LOFF(dkim.dkim_hash) },
+ { "dkim_identity", opt_stringptr, LOFF(dkim.dkim_identity) },
+ { "dkim_private_key", opt_stringptr, LOFF(dkim.dkim_private_key) },
+ { "dkim_selector", opt_stringptr, LOFF(dkim.dkim_selector) },
+ { "dkim_sign_headers", opt_stringptr, LOFF(dkim.dkim_sign_headers) },
+ { "dkim_strict", opt_stringptr, LOFF(dkim.dkim_strict) },
+ { "dkim_timestamps", opt_stringptr, LOFF(dkim.dkim_timestamps) },
+#endif
+ { "dns_qualify_single", opt_bool, LOFF(dns_qualify_single) },
+ { "dns_search_parents", opt_bool, LOFF(dns_search_parents) },
+ { "dnssec_request_domains", opt_stringptr, LOFF(dnssec.request) },
+ { "dnssec_require_domains", opt_stringptr, LOFF(dnssec.require) },
+ { "dscp", opt_stringptr, LOFF(dscp) },
+ { "fallback_hosts", opt_stringptr, LOFF(fallback_hosts) },
+ { "final_timeout", opt_time, LOFF(final_timeout) },
+ { "gethostbyname", opt_bool, LOFF(gethostbyname) },
+ { "helo_data", opt_stringptr, LOFF(helo_data) },
+#if !defined(DISABLE_TLS) && !defined(DISABLE_TLS_RESUME)
+ { "host_name_extract", opt_stringptr, LOFF(host_name_extract) },
+# endif
+ { "hosts", opt_stringptr, LOFF(hosts) },
+ { "hosts_avoid_esmtp", opt_stringptr, LOFF(hosts_avoid_esmtp) },
+ { "hosts_avoid_pipelining", opt_stringptr, LOFF(hosts_avoid_pipelining) },
+#ifndef DISABLE_TLS
+ { "hosts_avoid_tls", opt_stringptr, LOFF(hosts_avoid_tls) },
+#endif
+ { "hosts_max_try", opt_int, LOFF(hosts_max_try) },
+ { "hosts_max_try_hardlimit", opt_int, LOFF(hosts_max_try_hardlimit) },
+#ifndef DISABLE_TLS
+ { "hosts_nopass_tls", opt_stringptr, LOFF(hosts_nopass_tls) },
+ { "hosts_noproxy_tls", opt_stringptr, LOFF(hosts_noproxy_tls) },
+#endif
+ { "hosts_override", opt_bool, LOFF(hosts_override) },
+#ifndef DISABLE_PIPE_CONNECT
+ { "hosts_pipe_connect", opt_stringptr, LOFF(hosts_pipe_connect) },
+#endif
+ { "hosts_randomize", opt_bool, LOFF(hosts_randomize) },
+#if !defined(DISABLE_TLS) && !defined(DISABLE_OCSP)
+ { "hosts_request_ocsp", opt_stringptr, LOFF(hosts_request_ocsp) },
+#endif
+ { "hosts_require_alpn", opt_stringptr, LOFF(hosts_require_alpn) },
+ { "hosts_require_auth", opt_stringptr, LOFF(hosts_require_auth) },
+#ifndef DISABLE_TLS
+# ifdef SUPPORT_DANE
+ { "hosts_require_dane", opt_stringptr, LOFF(hosts_require_dane) },
+# endif
+# ifndef DISABLE_OCSP
+ { "hosts_require_ocsp", opt_stringptr, LOFF(hosts_require_ocsp) },
+# endif
+ { "hosts_require_tls", opt_stringptr, LOFF(hosts_require_tls) },
+#endif
+ { "hosts_try_auth", opt_stringptr, LOFF(hosts_try_auth) },
+ { "hosts_try_chunking", opt_stringptr, LOFF(hosts_try_chunking) },
+#ifdef SUPPORT_DANE
+ { "hosts_try_dane", opt_stringptr, LOFF(hosts_try_dane) },
+#endif
+ { "hosts_try_fastopen", opt_stringptr, LOFF(hosts_try_fastopen) },
+#ifndef DISABLE_PRDR
+ { "hosts_try_prdr", opt_stringptr, LOFF(hosts_try_prdr) },
+#endif
+#ifndef DISABLE_TLS
+ { "hosts_verify_avoid_tls", opt_stringptr, LOFF(hosts_verify_avoid_tls) },
+#endif
+ { "interface", opt_stringptr, LOFF(interface) },
+ { "keepalive", opt_bool, LOFF(keepalive) },
+ { "lmtp_ignore_quota", opt_bool, LOFF(lmtp_ignore_quota) },
+ { "max_rcpt", opt_int | opt_public,
+ OPT_OFF(transport_instance, max_addresses) },
+ { "message_linelength_limit", opt_int, LOFF(message_linelength_limit) },
+ { "multi_domain", opt_expand_bool | opt_public,
+ OPT_OFF(transport_instance, multi_domain) },
+ { "port", opt_stringptr, LOFF(port) },
+ { "protocol", opt_stringptr, LOFF(protocol) },
+ { "retry_include_ip_address", opt_expand_bool, LOFF(retry_include_ip_address) },
+ { "serialize_hosts", opt_stringptr, LOFF(serialize_hosts) },
+ { "size_addition", opt_int, LOFF(size_addition) },
+#ifdef SUPPORT_SOCKS
+ { "socks_proxy", opt_stringptr, LOFF(socks_proxy) },
+#endif
+#ifndef DISABLE_TLS
+ { "tls_alpn", opt_stringptr, LOFF(tls_alpn) },
+ { "tls_certificate", opt_stringptr, LOFF(tls_certificate) },
+ { "tls_crl", opt_stringptr, LOFF(tls_crl) },
+ { "tls_dh_min_bits", opt_int, LOFF(tls_dh_min_bits) },
+ { "tls_privatekey", opt_stringptr, LOFF(tls_privatekey) },
+ { "tls_require_ciphers", opt_stringptr, LOFF(tls_require_ciphers) },
+# ifndef DISABLE_TLS_RESUME
+ { "tls_resumption_hosts", opt_stringptr, LOFF(tls_resumption_hosts) },
+# endif
+ { "tls_sni", opt_stringptr, LOFF(tls_sni) },
+ { "tls_tempfail_tryclear", opt_bool, LOFF(tls_tempfail_tryclear) },
+ { "tls_try_verify_hosts", opt_stringptr, LOFF(tls_try_verify_hosts) },
+ { "tls_verify_cert_hostnames", opt_stringptr, LOFF(tls_verify_cert_hostnames)},
+ { "tls_verify_certificates", opt_stringptr, LOFF(tls_verify_certificates) },
+ { "tls_verify_hosts", opt_stringptr, LOFF(tls_verify_hosts) },
+#endif
+#ifdef SUPPORT_I18N
+ { "utf8_downconvert", opt_stringptr, LOFF(utf8_downconvert) },
+#endif
+};
+
+/* Size of the options list. An extern variable has to be used so that its
+address can appear in the tables drtables.c. */
+
+int smtp_transport_options_count = nelem(smtp_transport_options);
+
+
+#ifdef MACRO_PREDEF
+
+/* Dummy values */
+smtp_transport_options_block smtp_transport_option_defaults = {0};
+void smtp_transport_init(transport_instance *tblock) {}
+BOOL smtp_transport_entry(transport_instance *tblock, address_item *addr) {return FALSE;}
+void smtp_transport_closedown(transport_instance *tblock) {}
+
+#else /*!MACRO_PREDEF*/
+
+
+/* Default private options block for the smtp transport. */
+
+smtp_transport_options_block smtp_transport_option_defaults = {
+ /* All non-mentioned elements 0/NULL/FALSE */
+ .helo_data = US"$primary_hostname",
+ .protocol = US"smtp",
+ .hosts_try_chunking = US"*",
+#ifdef SUPPORT_DANE
+ .hosts_try_dane = US"*",
+#endif
+ .hosts_try_fastopen = US"*",
+#ifndef DISABLE_PRDR
+ .hosts_try_prdr = US"*",
+#endif
+#ifndef DISABLE_OCSP
+ .hosts_request_ocsp = US"*", /* hosts_request_ocsp (except under DANE; tls_client_start()) */
+#endif
+ .command_timeout = 5*60,
+ .connect_timeout = 5*60,
+ .data_timeout = 5*60,
+ .final_timeout = 10*60,
+ .size_addition = 1024,
+ .hosts_max_try = 5,
+ .hosts_max_try_hardlimit = 50,
+ .message_linelength_limit = 998,
+ .address_retry_include_sender = TRUE,
+ .dns_qualify_single = TRUE,
+ .dnssec = { .request= US"*", .require=NULL },
+ .delay_after_cutoff = TRUE,
+ .keepalive = TRUE,
+ .retry_include_ip_address = TRUE,
+#ifndef DISABLE_TLS
+ .tls_verify_certificates = US"system",
+ .tls_dh_min_bits = EXIM_CLIENT_DH_DEFAULT_MIN_BITS,
+ .tls_tempfail_tryclear = TRUE,
+ .tls_try_verify_hosts = US"*",
+ .tls_verify_cert_hostnames = US"*",
+# ifndef DISABLE_TLS_RESUME
+ .host_name_extract = US"${if and {{match{$host}{.outlook.com\\$}} {match{$item}{\\N^250-([\\w.]+)\\s\\N}}} {$1}}",
+# endif
+#endif
+#ifdef SUPPORT_I18N
+ .utf8_downconvert = US"-1",
+#endif
+#ifndef DISABLE_DKIM
+ .dkim =
+ { .dkim_hash = US"sha256", },
+#endif
+};
+
+/* some DSN flags for use later */
+
+static int rf_list[] = {rf_notify_never, rf_notify_success,
+ rf_notify_failure, rf_notify_delay };
+
+static uschar *rf_names[] = { US"NEVER", US"SUCCESS", US"FAILURE", US"DELAY" };
+
+
+
+/* Local statics */
+
+static uschar *smtp_command; /* Points to last cmd for error messages */
+static uschar *mail_command; /* Points to MAIL cmd for error messages */
+static uschar *data_command = US""; /* Points to DATA cmd for error messages */
+static BOOL update_waiting; /* TRUE to update the "wait" database */
+
+/*XXX move to smtp_context */
+static BOOL pipelining_active; /* current transaction is in pipe mode */
+
+
+static unsigned ehlo_response(uschar * buf, unsigned checks);
+
+
+/******************************************************************************/
+
+void
+smtp_deliver_init(void)
+{
+struct list
+ {
+ const pcre2_code ** re;
+ const uschar * string;
+ } list[] =
+ {
+ { &regex_AUTH, AUTHS_REGEX },
+ { &regex_CHUNKING, US"\\n250[\\s\\-]CHUNKING(\\s|\\n|$)" },
+ { &regex_DSN, US"\\n250[\\s\\-]DSN(\\s|\\n|$)" },
+ { &regex_IGNOREQUOTA, US"\\n250[\\s\\-]IGNOREQUOTA(\\s|\\n|$)" },
+ { &regex_PIPELINING, US"\\n250[\\s\\-]PIPELINING(\\s|\\n|$)" },
+ { &regex_SIZE, US"\\n250[\\s\\-]SIZE(\\s|\\n|$)" },
+
+#ifndef DISABLE_TLS
+ { &regex_STARTTLS, US"\\n250[\\s\\-]STARTTLS(\\s|\\n|$)" },
+#endif
+#ifndef DISABLE_PRDR
+ { &regex_PRDR, US"\\n250[\\s\\-]PRDR(\\s|\\n|$)" },
+#endif
+#ifdef SUPPORT_I18N
+ { &regex_UTF8, US"\\n250[\\s\\-]SMTPUTF8(\\s|\\n|$)" },
+#endif
+#ifndef DISABLE_PIPE_CONNECT
+ { &regex_EARLY_PIPE, US"\\n250[\\s\\-]" EARLY_PIPE_FEATURE_NAME "(\\s|\\n|$)" },
+#endif
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+ { &regex_LIMITS, US"\\n250[\\s\\-]LIMITS\\s" },
+#endif
+ };
+
+for (struct list * l = list; l < list + nelem(list); l++)
+ if (!*l->re)
+ *l->re = regex_must_compile(l->string, FALSE, TRUE);
+}
+
+
+/*************************************************
+* Setup entry point *
+*************************************************/
+
+/* This function is called when the transport is about to be used,
+but before running it in a sub-process. It is used for two things:
+
+ (1) To set the fallback host list in addresses, when delivering.
+ (2) To pass back the interface, port, protocol, and other options, for use
+ during callout verification.
+
+Arguments:
+ tblock pointer to the transport instance block
+ addrlist list of addresses about to be transported
+ tf if not NULL, pointer to block in which to return options
+ uid the uid that will be set (not used)
+ gid the gid that will be set (not used)
+ errmsg place for error message (not used)
+
+Returns: OK always (FAIL, DEFER not used)
+*/
+
+static int
+smtp_transport_setup(transport_instance *tblock, address_item *addrlist,
+ transport_feedback *tf, uid_t uid, gid_t gid, uschar **errmsg)
+{
+smtp_transport_options_block *ob = SOB tblock->options_block;
+
+/* Pass back options if required. This interface is getting very messy. */
+
+if (tf)
+ {
+ tf->interface = ob->interface;
+ tf->port = ob->port;
+ tf->protocol = ob->protocol;
+ tf->hosts = ob->hosts;
+ tf->hosts_override = ob->hosts_override;
+ tf->hosts_randomize = ob->hosts_randomize;
+ tf->gethostbyname = ob->gethostbyname;
+ tf->qualify_single = ob->dns_qualify_single;
+ tf->search_parents = ob->dns_search_parents;
+ tf->helo_data = ob->helo_data;
+ }
+
+/* Set the fallback host list for all the addresses that don't have fallback
+host lists, provided that the local host wasn't present in the original host
+list. */
+
+if (!testflag(addrlist, af_local_host_removed))
+ for (; addrlist; addrlist = addrlist->next)
+ if (!addrlist->fallback_hosts) addrlist->fallback_hosts = ob->fallback_hostlist;
+
+return OK;
+}
+
+
+
+/*************************************************
+* 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.
+
+Argument: pointer to the transport instance block
+Returns: nothing
+*/
+
+void
+smtp_transport_init(transport_instance *tblock)
+{
+smtp_transport_options_block *ob = SOB tblock->options_block;
+int old_pool = store_pool;
+
+/* Retry_use_local_part defaults FALSE if unset */
+
+if (tblock->retry_use_local_part == TRUE_UNSET)
+ tblock->retry_use_local_part = FALSE;
+
+/* Set the default port according to the protocol */
+
+if (!ob->port)
+ ob->port = strcmpic(ob->protocol, US"lmtp") == 0
+ ? US"lmtp"
+ : strcmpic(ob->protocol, US"smtps") == 0
+ ? US"smtps" : US"smtp";
+
+/* Set up the setup entry point, to be called before subprocesses for this
+transport. */
+
+tblock->setup = smtp_transport_setup;
+
+/* Complain if any of the timeouts are zero. */
+
+if (ob->command_timeout <= 0 || ob->data_timeout <= 0 ||
+ ob->final_timeout <= 0)
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG,
+ "command, data, or final timeout value is zero for %s transport",
+ tblock->name);
+
+/* If hosts_override is set and there are local hosts, set the global
+flag that stops verify from showing router hosts. */
+
+if (ob->hosts_override && ob->hosts) tblock->overrides_hosts = TRUE;
+
+/* If there are any fallback hosts listed, build a chain of host items
+for them, but do not do any lookups at this time. */
+
+store_pool = POOL_PERM;
+host_build_hostlist(&ob->fallback_hostlist, ob->fallback_hosts, FALSE);
+store_pool = old_pool;
+}
+
+
+
+
+
+/*************************************************
+* Set delivery info into all active addresses *
+*************************************************/
+
+/* Only addresses whose status is >= PENDING are relevant. A lesser
+status means that an address is not currently being processed.
+
+Arguments:
+ addrlist points to a chain of addresses
+ errno_value to put in each address's errno field
+ msg to put in each address's message field
+ rc to put in each address's transport_return field
+ pass_message if TRUE, set the "pass message" flag in the address
+ host if set, mark addrs as having used this host
+ smtp_greeting from peer
+ helo_response from peer
+ start points to timestamp of delivery start
+
+If errno_value has the special value ERRNO_CONNECTTIMEOUT, ETIMEDOUT is put in
+the errno field, and RTEF_CTOUT is ORed into the more_errno field, to indicate
+this particular type of timeout.
+
+Returns: nothing
+*/
+
+static void
+set_errno(address_item *addrlist, int errno_value, uschar *msg, int rc,
+ BOOL pass_message, host_item * host,
+#ifdef EXPERIMENTAL_DSN_INFO
+ const uschar * smtp_greeting, const uschar * helo_response,
+#endif
+ struct timeval * start
+ )
+{
+int orvalue = 0;
+struct timeval deliver_time;
+
+if (errno_value == ERRNO_CONNECTTIMEOUT)
+ {
+ errno_value = ETIMEDOUT;
+ orvalue = RTEF_CTOUT;
+ }
+timesince(&deliver_time, start);
+
+for (address_item * addr = addrlist; addr; addr = addr->next)
+ if (addr->transport_return >= PENDING)
+ {
+ addr->basic_errno = errno_value;
+ addr->more_errno |= orvalue;
+ addr->delivery_time = deliver_time;
+ if (msg)
+ {
+ addr->message = msg;
+ if (pass_message) setflag(addr, af_pass_message);
+ }
+ addr->transport_return = rc;
+ if (host)
+ {
+ addr->host_used = host;
+#ifdef EXPERIMENTAL_DSN_INFO
+ if (smtp_greeting)
+ {uschar * s = Ustrchr(smtp_greeting, '\n'); if (s) *s = '\0';}
+ addr->smtp_greeting = smtp_greeting;
+
+ if (helo_response)
+ {uschar * s = Ustrchr(helo_response, '\n'); if (s) *s = '\0';}
+ addr->helo_response = helo_response;
+#endif
+ }
+ }
+}
+
+static void
+set_errno_nohost(address_item * addrlist, int errno_value, uschar * msg, int rc,
+ BOOL pass_message, struct timeval * start)
+{
+set_errno(addrlist, errno_value, msg, rc, pass_message, NULL,
+#ifdef EXPERIMENTAL_DSN_INFO
+ NULL, NULL,
+#endif
+ start);
+}
+
+
+/*************************************************
+* Check an SMTP response *
+*************************************************/
+
+/* This function is given an errno code and the SMTP response buffer
+to analyse, together with the host identification for generating messages. It
+sets an appropriate message and puts the first digit of the response code into
+the yield variable. If no response was actually read, a suitable digit is
+chosen.
+
+Arguments:
+ host the current host, to get its name for messages
+ errno_value pointer to the errno value
+ more_errno from the top address for use with ERRNO_FILTER_FAIL
+ buffer the SMTP response buffer
+ yield where to put a one-digit SMTP response code
+ message where to put an error message
+ pass_message set TRUE if message is an SMTP response
+
+Returns: TRUE if an SMTP "QUIT" command should be sent, else FALSE
+*/
+
+static BOOL
+check_response(host_item *host, int *errno_value, int more_errno,
+ uschar *buffer, int *yield, uschar **message, BOOL *pass_message)
+{
+uschar * pl = pipelining_active ? US"pipelined " : US"";
+const uschar * s;
+
+*yield = '4'; /* Default setting is to give a temporary error */
+
+switch(*errno_value)
+ {
+ case ETIMEDOUT: /* Handle response timeout */
+ *message = US string_sprintf("SMTP timeout after %s%s",
+ pl, smtp_command);
+ if (transport_count > 0)
+ *message = US string_sprintf("%s (%d bytes written)", *message,
+ transport_count);
+ return FALSE;
+
+ case ERRNO_SMTPFORMAT: /* Handle malformed SMTP response */
+ s = string_printing(buffer);
+ while (isspace(*s)) s++;
+ *message = *s == 0
+ ? string_sprintf("Malformed SMTP reply (an empty line) "
+ "in response to %s%s", pl, smtp_command)
+ : string_sprintf("Malformed SMTP reply in response to %s%s: %s",
+ pl, smtp_command, s);
+ return FALSE;
+
+ case ERRNO_TLSFAILURE: /* Handle bad first read; can happen with
+ GnuTLS and TLS1.3 */
+ *message = US"bad first read from TLS conn";
+ return TRUE;
+
+ case ERRNO_FILTER_FAIL: /* Handle a failed filter process error;
+ can't send QUIT as we mustn't end the DATA. */
+ *message = string_sprintf("transport filter process failed (%d)%s",
+ more_errno,
+ more_errno == EX_EXECFAILED ? ": unable to execute command" : "");
+ return FALSE;
+
+ case ERRNO_CHHEADER_FAIL: /* Handle a failed add_headers expansion;
+ can't send QUIT as we mustn't end the DATA. */
+ *message =
+ string_sprintf("failed to expand headers_add or headers_remove: %s",
+ expand_string_message);
+ return FALSE;
+
+ case ERRNO_WRITEINCOMPLETE: /* failure to write a complete data block */
+ *message = US"failed to write a data block";
+ return FALSE;
+
+#ifdef SUPPORT_I18N
+ case ERRNO_UTF8_FWD: /* no advertised SMTPUTF8, for international message */
+ *message = US"utf8 support required but not offered for forwarding";
+ DEBUG(D_deliver|D_transport) debug_printf("%s\n", *message);
+ return TRUE;
+#endif
+ }
+
+/* Handle error responses from the remote mailer. */
+
+if (buffer[0] != 0)
+ {
+ *message = string_sprintf("SMTP error from remote mail server after %s%s: "
+ "%s", pl, smtp_command, s = string_printing(buffer));
+ *pass_message = TRUE;
+ *yield = buffer[0];
+ return TRUE;
+ }
+
+/* No data was read. If there is no errno, this must be the EOF (i.e.
+connection closed) case, which causes deferral. An explicit connection reset
+error has the same effect. Otherwise, put the host's identity in the message,
+leaving the errno value to be interpreted as well. In all cases, we have to
+assume the connection is now dead. */
+
+if (*errno_value == 0 || *errno_value == ECONNRESET)
+ {
+ *errno_value = ERRNO_SMTPCLOSED;
+ *message = US string_sprintf("Remote host closed connection "
+ "in response to %s%s", pl, smtp_command);
+ }
+else
+ *message = US string_sprintf("%s [%s]", host->name, host->address);
+
+return FALSE;
+}
+
+
+
+/*************************************************
+* Write error message to logs *
+*************************************************/
+
+/* This writes to the main log and to the message log.
+
+Arguments:
+ host the current host
+ detail the current message (addr_item->message)
+ basic_errno the errno (addr_item->basic_errno)
+
+Returns: nothing
+*/
+
+static void
+write_logs(const host_item *host, const uschar *suffix, int basic_errno)
+{
+gstring * message = LOGGING(outgoing_port)
+ ? string_fmt_append(NULL, "H=%s [%s]:%d", host->name, host->address,
+ host->port == PORT_NONE ? 25 : host->port)
+ : string_fmt_append(NULL, "H=%s [%s]", host->name, host->address);
+
+if (suffix)
+ {
+ message = string_fmt_append(message, ": %s", suffix);
+ if (basic_errno > 0)
+ message = string_fmt_append(message, ": %s", strerror(basic_errno));
+ }
+else
+ message = string_fmt_append(message, " %s", exim_errstr(basic_errno));
+
+log_write(0, LOG_MAIN, "%s", string_from_gstring(message));
+deliver_msglog("%s %s\n", tod_stamp(tod_log), message->s);
+}
+
+static void
+msglog_line(host_item * host, uschar * message)
+{
+deliver_msglog("%s H=%s [%s] %s\n", tod_stamp(tod_log),
+ host->name, host->address, message);
+}
+
+
+
+#ifndef DISABLE_EVENT
+/*************************************************
+* Post-defer action *
+*************************************************/
+
+/* This expands an arbitrary per-transport string.
+ It might, for example, be used to write to the database log.
+
+Arguments:
+ addr the address item containing error information
+ host the current host
+ evstr the event
+
+Returns: nothing
+*/
+
+static void
+deferred_event_raise(address_item * addr, host_item * host, uschar * evstr)
+{
+uschar * action = addr->transport->event_action;
+const uschar * save_domain;
+uschar * save_local;
+
+if (!action)
+ return;
+
+save_domain = deliver_domain;
+save_local = deliver_localpart;
+
+/*XXX would ip & port already be set up? */
+deliver_host_address = string_copy(host->address);
+deliver_host_port = host->port == PORT_NONE ? 25 : host->port;
+event_defer_errno = addr->basic_errno;
+
+router_name = addr->router->name;
+transport_name = addr->transport->name;
+deliver_domain = addr->domain;
+deliver_localpart = addr->local_part;
+
+(void) event_raise(action, evstr,
+ addr->message
+ ? addr->basic_errno > 0
+ ? string_sprintf("%s: %s", addr->message, strerror(addr->basic_errno))
+ : string_copy(addr->message)
+ : addr->basic_errno > 0
+ ? string_copy(US strerror(addr->basic_errno))
+ : NULL,
+ NULL);
+
+deliver_localpart = save_local;
+deliver_domain = save_domain;
+router_name = transport_name = NULL;
+}
+#endif
+
+/*************************************************
+* Reap SMTP specific responses *
+*************************************************/
+static int
+smtp_discard_responses(smtp_context * sx, smtp_transport_options_block * ob,
+ int count)
+{
+uschar flushbuffer[4096];
+
+while (count-- > 0)
+ {
+ if (!smtp_read_response(sx, flushbuffer, sizeof(flushbuffer),
+ '2', ob->command_timeout)
+ && (errno != 0 || flushbuffer[0] == 0))
+ break;
+ }
+return count;
+}
+
+
+/* Return boolean success */
+
+static BOOL
+smtp_reap_banner(smtp_context * sx)
+{
+BOOL good_response;
+#if defined(__linux__) && defined(TCP_QUICKACK)
+ { /* Hack to get QUICKACK disabled; has to be right after 3whs, and has to on->off */
+ int sock = sx->cctx.sock;
+ struct pollfd p = {.fd = sock, .events = POLLOUT};
+ if (poll(&p, 1, 1000) >= 0) /* retval test solely for compiler quitening */
+ {
+ (void) setsockopt(sock, IPPROTO_TCP, TCP_QUICKACK, US &on, sizeof(on));
+ (void) setsockopt(sock, IPPROTO_TCP, TCP_QUICKACK, US &off, sizeof(off));
+ }
+ }
+#endif
+good_response = smtp_read_response(sx, sx->buffer, sizeof(sx->buffer),
+ '2', (SOB sx->conn_args.ob)->command_timeout);
+#ifdef EXPERIMENTAL_DSN_INFO
+sx->smtp_greeting = string_copy(sx->buffer);
+#endif
+return good_response;
+}
+
+static BOOL
+smtp_reap_ehlo(smtp_context * sx)
+{
+if (!smtp_read_response(sx, sx->buffer, sizeof(sx->buffer), '2',
+ (SOB sx->conn_args.ob)->command_timeout))
+ {
+ if (errno != 0 || sx->buffer[0] == 0 || sx->lmtp)
+ {
+#ifdef EXPERIMENTAL_DSN_INFO
+ sx->helo_response = string_copy(sx->buffer);
+#endif
+ return FALSE;
+ }
+ sx->esmtp = FALSE;
+ }
+#ifdef EXPERIMENTAL_DSN_INFO
+sx->helo_response = string_copy(sx->buffer);
+#endif
+#ifndef DISABLE_EVENT
+(void) event_raise(sx->conn_args.tblock->event_action,
+ US"smtp:ehlo", sx->buffer, NULL);
+#endif
+return TRUE;
+}
+
+
+/* Grab a string differentiating server behind a loadbalancer, for TLS
+resumption when such servers do not share a session-cache */
+
+static void
+ehlo_response_lbserver(smtp_context * sx, smtp_transport_options_block * ob)
+{
+#if !defined(DISABLE_TLS) && !defined(DISABLE_TLS_RESUME)
+const uschar * s;
+uschar * save_item = iterate_item;
+
+if (sx->conn_args.have_lbserver)
+ return;
+iterate_item = sx->buffer;
+s = expand_cstring(ob->host_name_extract);
+iterate_item = save_item;
+sx->conn_args.host_lbserver = s && !*s ? NULL : s;
+sx->conn_args.have_lbserver = TRUE;
+#endif
+}
+
+
+
+/******************************************************************************/
+
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+/* If TLS, or TLS not offered, called with the EHLO response in the buffer.
+Check it for a LIMITS keyword and parse values into the smtp context structure.
+
+We don't bother with peers that we won't talk TLS to, even though they can,
+just ignore their LIMITS advice (if any) and treat them as if they do not.
+This saves us dealing with a duplicate set of values. */
+
+static void
+ehlo_response_limits_read(smtp_context * sx)
+{
+uschar * match;
+
+/* matches up to just after the first space after the keyword */
+
+if (regex_match(regex_LIMITS, sx->buffer, -1, &match))
+ for (const uschar * s = sx->buffer + Ustrlen(match); *s; )
+ {
+ while (isspace(*s)) s++;
+ if (*s == '\n') break;
+
+ if (strncmpic(s, US"MAILMAX=", 8) == 0)
+ {
+ sx->peer_limit_mail = atoi(CS (s += 8));
+ while (isdigit(*s)) s++;
+ }
+ else if (strncmpic(s, US"RCPTMAX=", 8) == 0)
+ {
+ sx->peer_limit_rcpt = atoi(CS (s += 8));
+ while (isdigit(*s)) s++;
+ }
+ else if (strncmpic(s, US"RCPTDOMAINMAX=", 14) == 0)
+ {
+ sx->peer_limit_rcptdom = atoi(CS (s += 14));
+ while (isdigit(*s)) s++;
+ }
+ else
+ while (*s && !isspace(*s)) s++;
+ }
+}
+
+/* Apply given values to the current connection */
+static void
+ehlo_limits_apply(smtp_context * sx,
+ unsigned limit_mail, unsigned limit_rcpt, unsigned limit_rcptdom)
+{
+if (limit_mail && limit_mail < sx->max_mail) sx->max_mail = limit_mail;
+if (limit_rcpt && limit_rcpt < sx->max_rcpt) sx->max_rcpt = limit_rcpt;
+if (limit_rcptdom)
+ {
+ DEBUG(D_transport) debug_printf("will treat as !multi_domain\n");
+ sx->single_rcpt_domain = TRUE;
+ }
+}
+
+/* Apply values from EHLO-resp to the current connection */
+static void
+ehlo_response_limits_apply(smtp_context * sx)
+{
+ehlo_limits_apply(sx, sx->peer_limit_mail, sx->peer_limit_rcpt,
+ sx->peer_limit_rcptdom);
+}
+
+/* Apply values read from cache to the current connection */
+static void
+ehlo_cache_limits_apply(smtp_context * sx)
+{
+# ifndef DISABLE_PIPE_CONNECT
+ehlo_limits_apply(sx, sx->ehlo_resp.limit_mail, sx->ehlo_resp.limit_rcpt,
+ sx->ehlo_resp.limit_rcptdom);
+# endif
+}
+#endif /*EXPERIMENTAL_ESMTP_LIMITS*/
+
+/******************************************************************************/
+
+#ifndef DISABLE_PIPE_CONNECT
+static uschar *
+ehlo_cache_key(const smtp_context * sx)
+{
+host_item * host = sx->conn_args.host;
+return Ustrchr(host->address, ':')
+ ? string_sprintf("[%s]:%d.EHLO", host->address,
+ host->port == PORT_NONE ? sx->port : host->port)
+ : string_sprintf("%s:%d.EHLO", host->address,
+ host->port == PORT_NONE ? sx->port : host->port);
+}
+
+/* Cache EHLO-response info for use by early-pipe.
+Called
+- During a normal flow on EHLO response (either cleartext or under TLS),
+ when we are willing to do PIPECONNECT and it is offered
+- During an early-pipe flow on receiving the actual EHLO response and noting
+ disparity versus the cached info used, when PIPECONNECT is still being offered
+
+We assume that suitable values have been set in the sx.ehlo_resp structure for
+features and auths; we handle the copy of limits. */
+
+static void
+write_ehlo_cache_entry(smtp_context * sx)
+{
+open_db dbblock, * dbm_file;
+
+# ifdef EXPERIMENTAL_ESMTP_LIMITS
+sx->ehlo_resp.limit_mail = sx->peer_limit_mail;
+sx->ehlo_resp.limit_rcpt = sx->peer_limit_rcpt;
+sx->ehlo_resp.limit_rcptdom = sx->peer_limit_rcptdom;
+# endif
+
+if ((dbm_file = dbfn_open(US"misc", O_RDWR, &dbblock, TRUE, TRUE)))
+ {
+ uschar * ehlo_resp_key = ehlo_cache_key(sx);
+ dbdata_ehlo_resp er = { .data = sx->ehlo_resp };
+
+ HDEBUG(D_transport)
+# ifdef EXPERIMENTAL_ESMTP_LIMITS
+ if (sx->ehlo_resp.limit_mail || sx->ehlo_resp.limit_rcpt || sx->ehlo_resp.limit_rcptdom)
+ debug_printf("writing clr %04x/%04x cry %04x/%04x lim %05d/%05d/%05d\n",
+ sx->ehlo_resp.cleartext_features, sx->ehlo_resp.cleartext_auths,
+ sx->ehlo_resp.crypted_features, sx->ehlo_resp.crypted_auths,
+ sx->ehlo_resp.limit_mail, sx->ehlo_resp.limit_rcpt,
+ sx->ehlo_resp.limit_rcptdom);
+ else
+# endif
+ debug_printf("writing clr %04x/%04x cry %04x/%04x\n",
+ sx->ehlo_resp.cleartext_features, sx->ehlo_resp.cleartext_auths,
+ sx->ehlo_resp.crypted_features, sx->ehlo_resp.crypted_auths);
+
+ dbfn_write(dbm_file, ehlo_resp_key, &er, (int)sizeof(er));
+ dbfn_close(dbm_file);
+ }
+}
+
+static void
+invalidate_ehlo_cache_entry(smtp_context * sx)
+{
+open_db dbblock, * dbm_file;
+
+if ( sx->early_pipe_active
+ && (dbm_file = dbfn_open(US"misc", O_RDWR, &dbblock, TRUE, TRUE)))
+ {
+ uschar * ehlo_resp_key = ehlo_cache_key(sx);
+ dbfn_delete(dbm_file, ehlo_resp_key);
+ dbfn_close(dbm_file);
+ }
+}
+
+static BOOL
+read_ehlo_cache_entry(smtp_context * sx)
+{
+open_db dbblock;
+open_db * dbm_file;
+
+if (!(dbm_file = dbfn_open(US"misc", O_RDONLY, &dbblock, FALSE, TRUE)))
+ { DEBUG(D_transport) debug_printf("ehlo-cache: no misc DB\n"); }
+else
+ {
+ uschar * ehlo_resp_key = ehlo_cache_key(sx);
+ dbdata_ehlo_resp * er;
+
+ if (!(er = dbfn_read_enforce_length(dbm_file, ehlo_resp_key, sizeof(dbdata_ehlo_resp))))
+ { DEBUG(D_transport) debug_printf("no ehlo-resp record\n"); }
+ else if (time(NULL) - er->time_stamp > retry_data_expire)
+ {
+ DEBUG(D_transport) debug_printf("ehlo-resp record too old\n");
+ dbfn_close(dbm_file);
+ if ((dbm_file = dbfn_open(US"misc", O_RDWR, &dbblock, TRUE, TRUE)))
+ dbfn_delete(dbm_file, ehlo_resp_key);
+ }
+ else
+ {
+ DEBUG(D_transport)
+# ifdef EXPERIMENTAL_ESMTP_LIMITS
+ if (er->data.limit_mail || er->data.limit_rcpt || er->data.limit_rcptdom)
+ debug_printf("EHLO response bits from cache:"
+ " cleartext 0x%04x/0x%04x crypted 0x%04x/0x%04x lim %05d/%05d/%05d\n",
+ er->data.cleartext_features, er->data.cleartext_auths,
+ er->data.crypted_features, er->data.crypted_auths,
+ er->data.limit_mail, er->data.limit_rcpt, er->data.limit_rcptdom);
+ else
+# endif
+ debug_printf("EHLO response bits from cache:"
+ " cleartext 0x%04x/0x%04x crypted 0x%04x/0x%04x\n",
+ er->data.cleartext_features, er->data.cleartext_auths,
+ er->data.crypted_features, er->data.crypted_auths);
+
+ sx->ehlo_resp = er->data;
+# ifdef EXPERIMENTAL_ESMTP_LIMITS
+ ehlo_cache_limits_apply(sx);
+# endif
+ dbfn_close(dbm_file);
+ return TRUE;
+ }
+ dbfn_close(dbm_file);
+ }
+return FALSE;
+}
+
+
+
+/* Return an auths bitmap for the set of AUTH methods offered by the server
+which match our authenticators. */
+
+static unsigned short
+study_ehlo_auths(smtp_context * sx)
+{
+uschar * names;
+auth_instance * au;
+uschar authnum;
+unsigned short authbits = 0;
+
+if (!sx->esmtp) return 0;
+if (!regex_AUTH) regex_AUTH = regex_must_compile(AUTHS_REGEX, FALSE, TRUE);
+if (!regex_match_and_setup(regex_AUTH, sx->buffer, 0, -1)) return 0;
+expand_nmax = -1; /* reset */
+names = string_copyn(expand_nstring[1], expand_nlength[1]);
+
+for (au = auths, authnum = 0; au; au = au->next, authnum++) if (au->client)
+ {
+ const uschar * list = names;
+ uschar * s;
+ for (int sep = ' '; s = string_nextinlist(&list, &sep, NULL, 0); )
+ if (strcmpic(au->public_name, s) == 0)
+ { authbits |= BIT(authnum); break; }
+ }
+
+DEBUG(D_transport)
+ debug_printf("server offers %s AUTH, methods '%s', bitmap 0x%04x\n",
+ tls_out.active.sock >= 0 ? "crypted" : "plaintext", names, authbits);
+
+if (tls_out.active.sock >= 0)
+ sx->ehlo_resp.crypted_auths = authbits;
+else
+ sx->ehlo_resp.cleartext_auths = authbits;
+return authbits;
+}
+
+
+
+
+/* Wait for and check responses for early-pipelining.
+
+Called from the lower-level smtp_read_response() function
+used for general code that assume synchronisation, if context
+flags indicate outstanding early-pipelining commands. Also
+called fom sync_responses() which handles pipelined commands.
+
+Arguments:
+ sx smtp connection context
+ countp number of outstanding responses, adjusted on return
+
+Return:
+ OK all well
+ DEFER error on first read of TLS'd conn
+ FAIL SMTP error in response
+*/
+int
+smtp_reap_early_pipe(smtp_context * sx, int * countp)
+{
+BOOL pending_BANNER = sx->pending_BANNER;
+BOOL pending_EHLO = sx->pending_EHLO;
+int rc = FAIL;
+
+sx->pending_BANNER = FALSE; /* clear early to avoid recursion */
+sx->pending_EHLO = FALSE;
+
+if (pending_BANNER)
+ {
+ DEBUG(D_transport) debug_printf("%s expect banner\n", __FUNCTION__);
+ (*countp)--;
+ if (!smtp_reap_banner(sx))
+ {
+ DEBUG(D_transport) debug_printf("bad banner\n");
+ if (tls_out.active.sock >= 0) rc = DEFER;
+ goto fail;
+ }
+ /*XXX EXPERIMENTAL_ESMTP_LIMITS ? */
+ ehlo_response_lbserver(sx, sx->conn_args.ob);
+ }
+
+if (pending_EHLO)
+ {
+ unsigned peer_offered;
+ unsigned short authbits = 0, * ap;
+
+ DEBUG(D_transport) debug_printf("%s expect ehlo\n", __FUNCTION__);
+ (*countp)--;
+ if (!smtp_reap_ehlo(sx))
+ {
+ DEBUG(D_transport) debug_printf("bad response for EHLO\n");
+ if (tls_out.active.sock >= 0) rc = DEFER;
+ goto fail;
+ }
+
+ /* Compare the actual EHLO response extensions and AUTH methods to the cached
+ value we assumed; on difference, dump or rewrite the cache and arrange for a
+ retry. */
+
+ ap = tls_out.active.sock < 0
+ ? &sx->ehlo_resp.cleartext_auths : &sx->ehlo_resp.crypted_auths;
+
+ peer_offered = ehlo_response(sx->buffer,
+ (tls_out.active.sock < 0 ? OPTION_TLS : 0)
+ | OPTION_CHUNKING | OPTION_PRDR | OPTION_DSN | OPTION_PIPE | OPTION_SIZE
+ | OPTION_UTF8 | OPTION_EARLY_PIPE
+ );
+# ifdef EXPERIMENTAL_ESMTP_LIMITS
+ if (tls_out.active.sock >= 0 || !(peer_offered & OPTION_TLS))
+ ehlo_response_limits_read(sx);
+# endif
+ if ( peer_offered != sx->peer_offered
+ || (authbits = study_ehlo_auths(sx)) != *ap)
+ {
+ HDEBUG(D_transport)
+ debug_printf("EHLO %s extensions changed, 0x%04x/0x%04x -> 0x%04x/0x%04x\n",
+ tls_out.active.sock < 0 ? "cleartext" : "crypted",
+ sx->peer_offered, *ap, peer_offered, authbits);
+ if (peer_offered & OPTION_EARLY_PIPE)
+ {
+ *(tls_out.active.sock < 0
+ ? &sx->ehlo_resp.cleartext_features : &sx->ehlo_resp.crypted_features) =
+ peer_offered;
+ *ap = authbits;
+ write_ehlo_cache_entry(sx);
+ }
+ else
+ invalidate_ehlo_cache_entry(sx);
+
+ return OK; /* just carry on */
+ }
+# ifdef EXPERIMENTAL_ESMTP_LIMITS
+ /* If we are handling LIMITS, compare the actual EHLO LIMITS values with the
+ cached values and invalidate cache if different. OK to carry on with
+ connect since values are advisory. */
+ {
+ if ( (tls_out.active.sock >= 0 || !(peer_offered & OPTION_TLS))
+ && ( sx->peer_limit_mail != sx->ehlo_resp.limit_mail
+ || sx->peer_limit_rcpt != sx->ehlo_resp.limit_rcpt
+ || sx->peer_limit_rcptdom != sx->ehlo_resp.limit_rcptdom
+ ) )
+ {
+ HDEBUG(D_transport)
+ {
+ debug_printf("EHLO LIMITS changed:");
+ if (sx->peer_limit_mail != sx->ehlo_resp.limit_mail)
+ debug_printf(" MAILMAX %u -> %u\n", sx->ehlo_resp.limit_mail, sx->peer_limit_mail);
+ else if (sx->peer_limit_rcpt != sx->ehlo_resp.limit_rcpt)
+ debug_printf(" RCPTMAX %u -> %u\n", sx->ehlo_resp.limit_rcpt, sx->peer_limit_rcpt);
+ else
+ debug_printf(" RCPTDOMAINMAX %u -> %u\n", sx->ehlo_resp.limit_rcptdom, sx->peer_limit_rcptdom);
+ }
+ invalidate_ehlo_cache_entry(sx);
+ }
+ }
+# endif
+ }
+return OK;
+
+fail:
+ invalidate_ehlo_cache_entry(sx);
+ (void) smtp_discard_responses(sx, sx->conn_args.ob, *countp);
+ return rc;
+}
+#endif /*!DISABLE_PIPE_CONNECT*/
+
+
+/*************************************************
+* Synchronize SMTP responses *
+*************************************************/
+
+/* This function is called from smtp_deliver() to receive SMTP responses from
+the server, and match them up with the commands to which they relate. When
+PIPELINING is not in use, this function is called after every command, and is
+therefore somewhat over-engineered, but it is simpler to use a single scheme
+that works both with and without PIPELINING instead of having two separate sets
+of code.
+
+The set of commands that are buffered up with pipelining may start with MAIL
+and may end with DATA; in between are RCPT commands that correspond to the
+addresses whose status is PENDING_DEFER. All other commands (STARTTLS, AUTH,
+etc.) are never buffered.
+
+Errors after MAIL or DATA abort the whole process leaving the response in the
+buffer. After MAIL, pending responses are flushed, and the original command is
+re-instated in big_buffer for error messages. For RCPT commands, the remote is
+permitted to reject some recipient addresses while accepting others. However
+certain errors clearly abort the whole process. Set the value in
+transport_return to PENDING_OK if the address is accepted. If there is a
+subsequent general error, it will get reset accordingly. If not, it will get
+converted to OK at the end.
+
+Arguments:
+ sx smtp connection context
+ count the number of responses to read
+ pending_DATA 0 if last command sent was not DATA
+ +1 if previously had a good recipient
+ -1 if not previously had a good recipient
+
+Returns: 3 if at least one address had 2xx and one had 5xx
+ 2 if at least one address had 5xx but none had 2xx
+ 1 if at least one host had a 2xx response, but none had 5xx
+ 0 no address had 2xx or 5xx but no errors (all 4xx, or just DATA)
+ -1 timeout while reading RCPT response
+ -2 I/O or other non-response error for RCPT
+ -3 DATA or MAIL failed - errno and buffer set
+ -4 banner or EHLO failed (early-pipelining)
+ -5 banner or EHLO failed (early-pipelining, TLS)
+*/
+
+static int
+sync_responses(smtp_context * sx, int count, int pending_DATA)
+{
+address_item * addr = sx->sync_addr;
+smtp_transport_options_block * ob = sx->conn_args.ob;
+int yield = 0;
+
+#ifndef DISABLE_PIPE_CONNECT
+int rc;
+if ((rc = smtp_reap_early_pipe(sx, &count)) != OK)
+ return rc == FAIL ? -4 : -5;
+#endif
+
+/* Handle the response for a MAIL command. On error, reinstate the original
+command in big_buffer for error message use, and flush any further pending
+responses before returning, except after I/O errors and timeouts. */
+
+if (sx->pending_MAIL)
+ {
+ DEBUG(D_transport) debug_printf("%s expect mail\n", __FUNCTION__);
+ count--;
+ sx->pending_MAIL = sx->RCPT_452 = FALSE;
+ if (!smtp_read_response(sx, sx->buffer, sizeof(sx->buffer),
+ '2', ob->command_timeout))
+ {
+ DEBUG(D_transport) debug_printf("bad response for MAIL\n");
+ Ustrcpy(big_buffer, mail_command); /* Fits, because it came from there! */
+ if (errno == ERRNO_TLSFAILURE)
+ return -5;
+ if (errno == 0 && sx->buffer[0] != 0)
+ {
+ int save_errno = 0;
+ if (sx->buffer[0] == '4')
+ {
+ save_errno = ERRNO_MAIL4XX;
+ addr->more_errno |= ((sx->buffer[1] - '0')*10 + sx->buffer[2] - '0') << 8;
+ }
+ count = smtp_discard_responses(sx, ob, count);
+ errno = save_errno;
+ }
+
+ if (pending_DATA) count--; /* Number of RCPT responses to come */
+ while (count-- > 0) /* Mark any pending addrs with the host used */
+ {
+ while (addr->transport_return != PENDING_DEFER) addr = addr->next;
+ addr->host_used = sx->conn_args.host;
+ addr = addr->next;
+ }
+ return -3;
+ }
+ }
+
+if (pending_DATA) count--; /* Number of RCPT responses to come */
+
+/* Read and handle the required number of RCPT responses, matching each one up
+with an address by scanning for the next address whose status is PENDING_DEFER.
+*/
+
+while (count-- > 0)
+ {
+ while (addr->transport_return != PENDING_DEFER)
+ if (!(addr = addr->next))
+ return -2;
+
+ /* The address was accepted */
+ addr->host_used = sx->conn_args.host;
+
+ DEBUG(D_transport) debug_printf("%s expect rcpt for %s\n", __FUNCTION__, addr->address);
+ if (smtp_read_response(sx, sx->buffer, sizeof(sx->buffer),
+ '2', ob->command_timeout))
+ {
+ yield |= 1;
+ addr->transport_return = PENDING_OK;
+
+ /* If af_dr_retry_exists is set, there was a routing delay on this address;
+ ensure that any address-specific retry record is expunged. We do this both
+ for the basic key and for the version that also includes the sender. */
+
+ 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);
+ }
+ }
+
+ /* Error on first TLS read */
+
+ else if (errno == ERRNO_TLSFAILURE)
+ return -5;
+
+ /* Timeout while reading the response */
+
+ else if (errno == ETIMEDOUT)
+ {
+ uschar *message = string_sprintf("SMTP timeout after RCPT TO:<%s>",
+ transport_rcpt_address(addr, sx->conn_args.tblock->rcpt_include_affixes));
+ set_errno_nohost(sx->first_addr, ETIMEDOUT, message, DEFER, FALSE, &sx->delivery_start);
+ retry_add_item(addr, addr->address_retry_key, 0);
+ update_waiting = FALSE;
+ return -1;
+ }
+
+ /* Handle other errors in obtaining an SMTP response by returning -1. This
+ will cause all the addresses to be deferred. Restore the SMTP command in
+ big_buffer for which we are checking the response, so the error message
+ makes sense. */
+
+ else if (errno != 0 || sx->buffer[0] == 0)
+ {
+ gstring gs = { .size = big_buffer_size, .ptr = 0, .s = big_buffer }, * g = &gs;
+
+ /* Use taint-unchecked routines for writing into big_buffer, trusting
+ that we'll never expand it. */
+
+ g = string_fmt_append_f(g, SVFMT_TAINT_NOCHK, "RCPT TO:<%s>",
+ transport_rcpt_address(addr, sx->conn_args.tblock->rcpt_include_affixes));
+ string_from_gstring(g);
+ return -2;
+ }
+
+ /* Handle SMTP permanent and temporary response codes. */
+
+ else
+ {
+ addr->message =
+ string_sprintf("SMTP error from remote mail server after RCPT TO:<%s>: "
+ "%s", transport_rcpt_address(addr, sx->conn_args.tblock->rcpt_include_affixes),
+ string_printing(sx->buffer));
+ setflag(addr, af_pass_message);
+ if (!sx->verify)
+ msglog_line(sx->conn_args.host, addr->message);
+
+ /* The response was 5xx */
+
+ if (sx->buffer[0] == '5')
+ {
+ addr->transport_return = FAIL;
+ yield |= 2;
+ }
+
+ /* The response was 4xx */
+
+ else
+ {
+ addr->transport_return = DEFER;
+ addr->basic_errno = ERRNO_RCPT4XX;
+ addr->more_errno |= ((sx->buffer[1] - '0')*10 + sx->buffer[2] - '0') << 8;
+
+ if (!sx->verify)
+ {
+#ifndef DISABLE_EVENT
+ event_defer_errno = addr->more_errno;
+ msg_event_raise(US"msg:rcpt:host:defer", addr);
+#endif
+ /* If a 452 and we've had at least one 2xx or 5xx, set next_addr to the
+ start point for another MAIL command. */
+
+ if (addr->more_errno >> 8 == 52 && yield & 3)
+ {
+ if (!sx->RCPT_452) /* initialised at MAIL-ack above */
+ {
+ DEBUG(D_transport)
+ debug_printf("%s: seen first 452 too-many-rcpts\n", __FUNCTION__);
+ sx->RCPT_452 = TRUE;
+ sx->next_addr = addr;
+ }
+ addr->transport_return = PENDING_DEFER;
+ addr->basic_errno = 0;
+ }
+ else
+ {
+ /* Log temporary errors if there are more hosts to be tried.
+ If not, log this last one in the == line. */
+
+ if (sx->conn_args.host->next)
+ if (LOGGING(outgoing_port))
+ log_write(0, LOG_MAIN, "H=%s [%s]:%d %s", sx->conn_args.host->name,
+ sx->conn_args.host->address,
+ sx->port == PORT_NONE ? 25 : sx->port, addr->message);
+ else
+ log_write(0, LOG_MAIN, "H=%s [%s]: %s", sx->conn_args.host->name,
+ sx->conn_args.host->address, addr->message);
+
+#ifndef DISABLE_EVENT
+ else
+ msg_event_raise(US"msg:rcpt:defer", addr);
+#endif
+
+ /* Do not put this message on the list of those waiting for specific
+ hosts, as otherwise it is likely to be tried too often. */
+
+ update_waiting = FALSE;
+
+ /* Add a retry item for the address so that it doesn't get tried again
+ too soon. If address_retry_include_sender is true, add the sender address
+ to the retry key. */
+
+ retry_add_item(addr,
+ ob->address_retry_include_sender
+ ? string_sprintf("%s:<%s>", addr->address_retry_key, sender_address)
+ : addr->address_retry_key,
+ 0);
+ }
+ }
+ }
+ }
+ if (count && !(addr = addr->next))
+ return -2;
+ } /* Loop for next RCPT response */
+
+/* Update where to start at for the next block of responses, unless we
+have already handled all the addresses. */
+
+if (addr) sx->sync_addr = addr->next;
+
+/* Handle a response to DATA. If we have not had any good recipients, either
+previously or in this block, the response is ignored. */
+
+if (pending_DATA != 0)
+ {
+ DEBUG(D_transport) debug_printf("%s expect data\n", __FUNCTION__);
+ if (!smtp_read_response(sx, sx->buffer, sizeof(sx->buffer),
+ '3', ob->command_timeout))
+ {
+ int code;
+ uschar *msg;
+ BOOL pass_message;
+
+ if (errno == ERRNO_TLSFAILURE) /* Error on first TLS read */
+ return -5;
+
+ if (pending_DATA > 0 || (yield & 1) != 0)
+ {
+ if (errno == 0 && sx->buffer[0] == '4')
+ {
+ errno = ERRNO_DATA4XX;
+ sx->first_addr->more_errno |= ((sx->buffer[1] - '0')*10 + sx->buffer[2] - '0') << 8;
+ }
+ return -3;
+ }
+ (void)check_response(sx->conn_args.host, &errno, 0, sx->buffer, &code, &msg, &pass_message);
+ DEBUG(D_transport) debug_printf("%s\nerror for DATA ignored: pipelining "
+ "is in use and there were no good recipients\n", msg);
+ }
+ }
+
+/* All responses read and handled; MAIL (if present) received 2xx and DATA (if
+present) received 3xx. If any RCPTs were handled and yielded anything other
+than 4xx, yield will be set non-zero. */
+
+return yield;
+}
+
+
+
+
+
+/* Try an authenticator's client entry */
+
+static int
+try_authenticator(smtp_context * sx, auth_instance * au)
+{
+smtp_transport_options_block * ob = sx->conn_args.ob; /* transport options */
+host_item * host = sx->conn_args.host; /* host to deliver to */
+int rc;
+
+/* Set up globals for error messages */
+
+authenticator_name = au->name;
+driver_srcfile = au->srcfile;
+driver_srcline = au->srcline;
+
+sx->outblock.authenticating = TRUE;
+rc = (au->info->clientcode)(au, sx, ob->command_timeout,
+ sx->buffer, sizeof(sx->buffer));
+sx->outblock.authenticating = FALSE;
+driver_srcfile = authenticator_name = NULL; driver_srcline = 0;
+DEBUG(D_transport) debug_printf("%s authenticator yielded %d\n", au->name, rc);
+
+/* A temporary authentication failure must hold up delivery to
+this host. After a permanent authentication failure, we carry on
+to try other authentication methods. If all fail hard, try to
+deliver the message unauthenticated unless require_auth was set. */
+
+switch(rc)
+ {
+ case OK:
+ f.smtp_authenticated = TRUE; /* stops the outer loop */
+ client_authenticator = au->name;
+ if (au->set_client_id)
+ client_authenticated_id = expand_string(au->set_client_id);
+ break;
+
+ /* Failure after writing a command */
+
+ case FAIL_SEND:
+ return FAIL_SEND;
+
+ /* Failure after reading a response */
+
+ case FAIL:
+ if (errno != 0 || sx->buffer[0] != '5') return FAIL;
+ log_write(0, LOG_MAIN, "%s authenticator failed H=%s [%s] %s",
+ au->name, host->name, host->address, sx->buffer);
+ break;
+
+ /* Failure by some other means. In effect, the authenticator
+ decided it wasn't prepared to handle this case. Typically this
+ is the result of "fail" in an expansion string. Do we need to
+ log anything here? Feb 2006: a message is now put in the buffer
+ if logging is required. */
+
+ case CANCELLED:
+ if (*sx->buffer != 0)
+ log_write(0, LOG_MAIN, "%s authenticator cancelled "
+ "authentication H=%s [%s] %s", au->name, host->name,
+ host->address, sx->buffer);
+ break;
+
+ /* Internal problem, message in buffer. */
+
+ case ERROR:
+ set_errno_nohost(sx->addrlist, ERRNO_AUTHPROB, string_copy(sx->buffer),
+ DEFER, FALSE, &sx->delivery_start);
+ return ERROR;
+ }
+return OK;
+}
+
+
+
+
+/* Do the client side of smtp-level authentication.
+
+Arguments:
+ sx smtp connection context
+
+sx->buffer should have the EHLO response from server (gets overwritten)
+
+Returns:
+ OK Success, or failed (but not required): global "smtp_authenticated" set
+ DEFER Failed authentication (and was required)
+ ERROR Internal problem
+
+ FAIL_SEND Failed communications - transmit
+ FAIL - response
+*/
+
+static int
+smtp_auth(smtp_context * sx)
+{
+host_item * host = sx->conn_args.host; /* host to deliver to */
+smtp_transport_options_block * ob = sx->conn_args.ob; /* transport options */
+int require_auth = verify_check_given_host(CUSS &ob->hosts_require_auth, host);
+#ifndef DISABLE_PIPE_CONNECT
+unsigned short authbits = tls_out.active.sock >= 0
+ ? sx->ehlo_resp.crypted_auths : sx->ehlo_resp.cleartext_auths;
+#endif
+uschar * fail_reason = US"server did not advertise AUTH support";
+
+f.smtp_authenticated = FALSE;
+client_authenticator = client_authenticated_id = client_authenticated_sender = NULL;
+
+if (!regex_AUTH)
+ regex_AUTH = regex_must_compile(AUTHS_REGEX, FALSE, TRUE);
+
+/* Is the server offering AUTH? */
+
+if ( sx->esmtp
+ &&
+#ifndef DISABLE_PIPE_CONNECT
+ sx->early_pipe_active ? authbits
+ :
+#endif
+ regex_match_and_setup(regex_AUTH, sx->buffer, 0, -1)
+ )
+ {
+ uschar * names = NULL;
+ expand_nmax = -1; /* reset */
+
+#ifndef DISABLE_PIPE_CONNECT
+ if (!sx->early_pipe_active)
+#endif
+ names = string_copyn(expand_nstring[1], expand_nlength[1]);
+
+ /* Must not do this check until after we have saved the result of the
+ regex match above as the check could be another RE. */
+
+ if ( require_auth == OK
+ || verify_check_given_host(CUSS &ob->hosts_try_auth, host) == OK)
+ {
+ DEBUG(D_transport) debug_printf("scanning authentication mechanisms\n");
+ fail_reason = US"no common mechanisms were found";
+
+#ifndef DISABLE_PIPE_CONNECT
+ if (sx->early_pipe_active)
+ {
+ /* Scan our authenticators (which support use by a client and were offered
+ by the server (checked at cache-write time)), not suppressed by
+ client_condition. If one is found, attempt to authenticate by calling its
+ client function. We are limited to supporting up to 16 authenticator
+ public-names by the number of bits in a short. */
+
+ auth_instance * au;
+ uschar bitnum;
+ int rc;
+
+ for (bitnum = 0, au = auths;
+ !f.smtp_authenticated && au && bitnum < 16;
+ bitnum++, au = au->next) if (authbits & BIT(bitnum))
+ {
+ if ( au->client_condition
+ && !expand_check_condition(au->client_condition, au->name,
+ US"client authenticator"))
+ {
+ DEBUG(D_transport) debug_printf("skipping %s authenticator: %s\n",
+ au->name, "client_condition is false");
+ continue;
+ }
+
+ /* Found data for a listed mechanism. Call its client entry. Set
+ a flag in the outblock so that data is overwritten after sending so
+ that reflections don't show it. */
+
+ fail_reason = US"authentication attempt(s) failed";
+
+ if ((rc = try_authenticator(sx, au)) != OK)
+ return rc;
+ }
+ }
+ else
+#endif
+
+ /* Scan the configured authenticators looking for one which is configured
+ for use as a client, which is not suppressed by client_condition, and
+ whose name matches an authentication mechanism supported by the server.
+ If one is found, attempt to authenticate by calling its client function.
+ */
+
+ for (auth_instance * au = auths; !f.smtp_authenticated && au; au = au->next)
+ {
+ uschar *p = names;
+
+ if ( !au->client
+ || ( au->client_condition
+ && !expand_check_condition(au->client_condition, au->name,
+ US"client authenticator")))
+ {
+ DEBUG(D_transport) debug_printf("skipping %s authenticator: %s\n",
+ au->name,
+ (au->client)? "client_condition is false" :
+ "not configured as a client");
+ continue;
+ }
+
+ /* Loop to scan supported server mechanisms */
+
+ while (*p)
+ {
+ int len = Ustrlen(au->public_name);
+ int rc;
+
+ while (isspace(*p)) p++;
+
+ if (strncmpic(au->public_name, p, len) != 0 ||
+ (p[len] != 0 && !isspace(p[len])))
+ {
+ while (*p != 0 && !isspace(*p)) p++;
+ continue;
+ }
+
+ /* Found data for a listed mechanism. Call its client entry. Set
+ a flag in the outblock so that data is overwritten after sending so
+ that reflections don't show it. */
+
+ fail_reason = US"authentication attempt(s) failed";
+
+ if ((rc = try_authenticator(sx, au)) != OK)
+ return rc;
+
+ break; /* If not authenticated, try next authenticator */
+ } /* Loop for scanning supported server mechanisms */
+ } /* Loop for further authenticators */
+ }
+ }
+
+/* If we haven't authenticated, but are required to, give up. */
+
+if (require_auth == OK && !f.smtp_authenticated)
+ {
+#ifndef DISABLE_PIPE_CONNECT
+ invalidate_ehlo_cache_entry(sx);
+#endif
+ set_errno_nohost(sx->addrlist, ERRNO_AUTHFAIL,
+ string_sprintf("authentication required but %s", fail_reason), DEFER,
+ FALSE, &sx->delivery_start);
+ return DEFER;
+ }
+
+return OK;
+}
+
+
+/* Construct AUTH appendix string for MAIL TO */
+/*
+Arguments
+ sx context for smtp connection
+ p point in sx->buffer to build string
+ addrlist chain of potential addresses to deliver
+
+Globals f.smtp_authenticated
+ client_authenticated_sender
+Return True on error, otherwise buffer has (possibly empty) terminated string
+*/
+
+static BOOL
+smtp_mail_auth_str(smtp_context * sx, uschar * p, address_item * addrlist)
+{
+smtp_transport_options_block * ob = sx->conn_args.ob;
+uschar * local_authenticated_sender = authenticated_sender;
+
+#ifdef notdef
+ debug_printf("smtp_mail_auth_str: as<%s> os<%s> SA<%s>\n",
+ authenticated_sender, ob->authenticated_sender, f.smtp_authenticated?"Y":"N");
+#endif
+
+if (ob->authenticated_sender)
+ {
+ uschar * new = expand_string(ob->authenticated_sender);
+ if (!new)
+ {
+ if (!f.expand_string_forcedfail)
+ {
+ uschar *message = string_sprintf("failed to expand "
+ "authenticated_sender: %s", expand_string_message);
+ set_errno_nohost(addrlist, ERRNO_EXPANDFAIL, message, DEFER, FALSE, &sx->delivery_start);
+ return TRUE;
+ }
+ }
+ else if (*new)
+ local_authenticated_sender = new;
+ }
+
+/* Add the authenticated sender address if present */
+
+if ( (f.smtp_authenticated || ob->authenticated_sender_force)
+ && local_authenticated_sender)
+ {
+ string_format_nt(p, sizeof(sx->buffer) - (p-sx->buffer), " AUTH=%s",
+ auth_xtextencode(local_authenticated_sender,
+ Ustrlen(local_authenticated_sender)));
+ client_authenticated_sender = string_copy(local_authenticated_sender);
+ }
+else
+ *p = 0;
+
+return FALSE;
+}
+
+
+
+typedef struct smtp_compare_s
+{
+ uschar * current_sender_address;
+ struct transport_instance * tblock;
+} smtp_compare_t;
+
+
+/* Create a unique string that identifies this message, it is based on
+sender_address, helo_data and tls_certificate if enabled.
+*/
+
+static uschar *
+smtp_local_identity(uschar * sender, struct transport_instance * tblock)
+{
+address_item * addr1;
+uschar * if1 = US"";
+uschar * helo1 = US"";
+#ifndef DISABLE_TLS
+uschar * tlsc1 = US"";
+#endif
+uschar * save_sender_address = sender_address;
+uschar * local_identity = NULL;
+smtp_transport_options_block * ob = SOB tblock->options_block;
+
+sender_address = sender;
+
+addr1 = deliver_make_addr (sender, TRUE);
+deliver_set_expansions(addr1);
+
+if (ob->interface)
+ if1 = expand_string(ob->interface);
+
+if (ob->helo_data)
+ helo1 = expand_string(ob->helo_data);
+
+#ifndef DISABLE_TLS
+if (ob->tls_certificate)
+ tlsc1 = expand_string(ob->tls_certificate);
+local_identity = string_sprintf ("%s^%s^%s", if1, helo1, tlsc1);
+#else
+local_identity = string_sprintf ("%s^%s", if1, helo1);
+#endif
+
+deliver_set_expansions(NULL);
+sender_address = save_sender_address;
+
+return local_identity;
+}
+
+
+
+/* This routine is a callback that is called from transport_check_waiting.
+This function will evaluate the incoming message versus the previous
+message. If the incoming message is using a different local identity then
+we will veto this new message. */
+
+static BOOL
+smtp_are_same_identities(uschar * message_id, smtp_compare_t * s_compare)
+{
+uschar * message_local_identity,
+ * current_local_identity,
+ * new_sender_address;
+
+current_local_identity =
+ smtp_local_identity(s_compare->current_sender_address, s_compare->tblock);
+
+if (!(new_sender_address = spool_sender_from_msgid(message_id)))
+ return FALSE;
+
+
+message_local_identity =
+ smtp_local_identity(new_sender_address, s_compare->tblock);
+
+return Ustrcmp(current_local_identity, message_local_identity) == 0;
+}
+
+
+
+static unsigned
+ehlo_response(uschar * buf, unsigned checks)
+{
+PCRE2_SIZE bsize = Ustrlen(buf);
+pcre2_match_data * md = pcre2_match_data_create(1, pcre_gen_ctx);
+
+/* debug_printf("%s: check for 0x%04x\n", __FUNCTION__, checks); */
+
+#ifndef DISABLE_TLS
+if ( checks & OPTION_TLS
+ && pcre2_match(regex_STARTTLS,
+ (PCRE2_SPTR)buf, bsize, 0, PCRE_EOPT, md, pcre_mtc_ctx) < 0)
+#endif
+ checks &= ~OPTION_TLS;
+
+if ( checks & OPTION_IGNQ
+ && pcre2_match(regex_IGNOREQUOTA,
+ (PCRE2_SPTR)buf, bsize, 0, PCRE_EOPT, md, pcre_mtc_ctx) < 0)
+ checks &= ~OPTION_IGNQ;
+
+if ( checks & OPTION_CHUNKING
+ && pcre2_match(regex_CHUNKING,
+ (PCRE2_SPTR)buf, bsize, 0, PCRE_EOPT, md, pcre_mtc_ctx) < 0)
+ checks &= ~OPTION_CHUNKING;
+
+#ifndef DISABLE_PRDR
+if ( checks & OPTION_PRDR
+ && pcre2_match(regex_PRDR,
+ (PCRE2_SPTR)buf, bsize, 0, PCRE_EOPT, md, pcre_mtc_ctx) < 0)
+#endif
+ checks &= ~OPTION_PRDR;
+
+#ifdef SUPPORT_I18N
+if ( checks & OPTION_UTF8
+ && pcre2_match(regex_UTF8,
+ (PCRE2_SPTR)buf, bsize, 0, PCRE_EOPT, md, pcre_mtc_ctx) < 0)
+#endif
+ checks &= ~OPTION_UTF8;
+
+if ( checks & OPTION_DSN
+ && pcre2_match(regex_DSN,
+ (PCRE2_SPTR)buf, bsize, 0, PCRE_EOPT, md, pcre_mtc_ctx) < 0)
+ checks &= ~OPTION_DSN;
+
+if ( checks & OPTION_PIPE
+ && pcre2_match(regex_PIPELINING,
+ (PCRE2_SPTR)buf, bsize, 0, PCRE_EOPT, md, pcre_mtc_ctx) < 0)
+ checks &= ~OPTION_PIPE;
+
+if ( checks & OPTION_SIZE
+ && pcre2_match(regex_SIZE,
+ (PCRE2_SPTR)buf, bsize, 0, PCRE_EOPT, md, pcre_mtc_ctx) < 0)
+ checks &= ~OPTION_SIZE;
+
+#ifndef DISABLE_PIPE_CONNECT
+if ( checks & OPTION_EARLY_PIPE
+ && pcre2_match(regex_EARLY_PIPE,
+ (PCRE2_SPTR)buf, bsize, 0, PCRE_EOPT, md, pcre_mtc_ctx) < 0)
+#endif
+ checks &= ~OPTION_EARLY_PIPE;
+
+pcre2_match_data_free(md);
+/* debug_printf("%s: found 0x%04x\n", __FUNCTION__, checks); */
+return checks;
+}
+
+
+
+/* Callback for emitting a BDAT data chunk header.
+
+If given a nonzero size, first flush any buffered SMTP commands
+then emit the command.
+
+Reap previous SMTP command responses if requested, and always reap
+the response from a previous BDAT command.
+
+Args:
+ tctx transport context
+ chunk_size value for SMTP BDAT command
+ flags
+ tc_chunk_last add LAST option to SMTP BDAT command
+ tc_reap_prev reap response to previous SMTP commands
+
+Returns:
+ OK or ERROR
+ DEFER TLS error on first read (EHLO-resp); errno set
+*/
+
+static int
+smtp_chunk_cmd_callback(transport_ctx * tctx, unsigned chunk_size,
+ unsigned flags)
+{
+smtp_transport_options_block * ob = SOB tctx->tblock->options_block;
+smtp_context * sx = tctx->smtp_context;
+int cmd_count = 0;
+int prev_cmd_count;
+
+/* Write SMTP chunk header command. If not reaping responses, note that
+there may be more writes (like, the chunk data) done soon. */
+
+if (chunk_size > 0)
+ {
+#ifndef DISABLE_PIPE_CONNECT
+ BOOL new_conn = !!(sx->outblock.conn_args);
+#endif
+ if((cmd_count = smtp_write_command(sx,
+ flags & tc_reap_prev ? SCMD_FLUSH : SCMD_MORE,
+ "BDAT %u%s\r\n", chunk_size, flags & tc_chunk_last ? " LAST" : "")
+ ) < 0) return ERROR;
+ if (flags & tc_chunk_last)
+ data_command = string_copy(big_buffer); /* Save for later error message */
+#ifndef DISABLE_PIPE_CONNECT
+ /* That command write could have been the one that made the connection.
+ Copy the fd from the client conn ctx (smtp transport specific) to the
+ generic transport ctx. */
+
+ if (new_conn)
+ tctx->u.fd = sx->outblock.cctx->sock;
+#endif
+ }
+
+prev_cmd_count = cmd_count += sx->cmd_count;
+
+/* Reap responses for any previous, but not one we just emitted */
+
+if (chunk_size > 0)
+ prev_cmd_count--;
+if (sx->pending_BDAT)
+ prev_cmd_count--;
+
+if (flags & tc_reap_prev && prev_cmd_count > 0)
+ {
+ DEBUG(D_transport) debug_printf("look for %d responses"
+ " for previous pipelined cmds\n", prev_cmd_count);
+
+ switch(sync_responses(sx, prev_cmd_count, 0))
+ {
+ case 1: /* 2xx (only) => OK */
+ case 3: sx->good_RCPT = TRUE; /* 2xx & 5xx => OK & progress made */
+ case 2: sx->completed_addr = TRUE; /* 5xx (only) => progress made */
+ case 0: break; /* No 2xx or 5xx, but no probs */
+
+ case -5: errno = ERRNO_TLSFAILURE;
+ return DEFER;
+#ifndef DISABLE_PIPE_CONNECT
+ case -4: /* non-2xx for pipelined banner or EHLO */
+#endif
+ case -1: /* Timeout on RCPT */
+ default: return ERROR; /* I/O error, or any MAIL/DATA error */
+ }
+ cmd_count = 1;
+ if (!sx->pending_BDAT)
+ pipelining_active = FALSE;
+ }
+
+/* Reap response for an outstanding BDAT */
+
+if (sx->pending_BDAT)
+ {
+ DEBUG(D_transport) debug_printf("look for one response for BDAT\n");
+
+ if (!smtp_read_response(sx, sx->buffer, sizeof(sx->buffer), '2',
+ ob->command_timeout))
+ {
+ if (errno == 0 && sx->buffer[0] == '4')
+ {
+ errno = ERRNO_DATA4XX; /*XXX does this actually get used? */
+ sx->addrlist->more_errno |=
+ ((sx->buffer[1] - '0')*10 + sx->buffer[2] - '0') << 8;
+ }
+ return ERROR;
+ }
+ cmd_count--;
+ sx->pending_BDAT = FALSE;
+ pipelining_active = FALSE;
+ }
+else if (chunk_size > 0)
+ sx->pending_BDAT = TRUE;
+
+
+sx->cmd_count = cmd_count;
+return OK;
+}
+
+
+
+#ifdef SUPPORT_DANE
+static int
+check_force_dane_conn(smtp_context * sx, smtp_transport_options_block * ob)
+{
+int rc;
+if( sx->dane_required
+ || verify_check_given_host(CUSS &ob->hosts_try_dane, sx->conn_args.host) == OK
+ )
+ switch (rc = tlsa_lookup(sx->conn_args.host, &sx->conn_args.tlsa_dnsa, sx->dane_required))
+ {
+ case OK: sx->conn_args.dane = TRUE;
+ ob->tls_tempfail_tryclear = FALSE; /* force TLS */
+ ob->tls_sni = sx->conn_args.host->name; /* force SNI */
+ break;
+ case FAIL_FORCED: break;
+ default: set_errno_nohost(sx->addrlist, ERRNO_DNSDEFER,
+ string_sprintf("DANE error: tlsa lookup %s",
+ rc_to_string(rc)),
+ rc, FALSE, &sx->delivery_start);
+# ifndef DISABLE_EVENT
+ (void) event_raise(sx->conn_args.tblock->event_action,
+ US"dane:fail", sx->dane_required
+ ? US"dane-required" : US"dnssec-invalid",
+ NULL);
+# endif
+ return rc;
+ }
+return OK;
+}
+#endif
+
+
+/*************************************************
+* Make connection for given message *
+*************************************************/
+
+/*
+Arguments:
+ sx connection context
+ suppress_tls if TRUE, don't attempt a TLS connection - this is set for
+ a second attempt after TLS initialization fails
+
+Returns: OK - the connection was made and the delivery attempted;
+ fd is set in the conn context, tls_out set up.
+ DEFER - the connection could not be made, or something failed
+ while setting up the SMTP session, or there was a
+ non-message-specific error, such as a timeout.
+ ERROR - helo_data or add_headers or authenticated_sender is
+ specified for this transport, and the string failed
+ to expand
+*/
+int
+smtp_setup_conn(smtp_context * sx, BOOL suppress_tls)
+{
+smtp_transport_options_block * ob = sx->conn_args.tblock->options_block;
+BOOL pass_message = FALSE;
+uschar * message = NULL;
+int yield = OK;
+#ifndef DISABLE_TLS
+uschar * tls_errstr;
+#endif
+
+/* Many lines of clearing individual elements of *sx that used to
+be here have been replaced by a full memset to zero (de41aff051).
+There are two callers, this file and verify.c . Now we only set
+up nonzero elements. */
+
+sx->conn_args.ob = ob;
+
+sx->lmtp = strcmpic(ob->protocol, US"lmtp") == 0;
+sx->smtps = strcmpic(ob->protocol, US"smtps") == 0;
+sx->send_rset = TRUE;
+sx->send_quit = TRUE;
+sx->setting_up = TRUE;
+sx->esmtp = TRUE;
+sx->dsn_all_lasthop = TRUE;
+#ifdef SUPPORT_DANE
+sx->dane_required =
+ verify_check_given_host(CUSS &ob->hosts_require_dane, sx->conn_args.host) == OK;
+#endif
+
+if ((sx->max_mail = sx->conn_args.tblock->connection_max_messages) == 0) sx->max_mail = 999999;
+if ((sx->max_rcpt = sx->conn_args.tblock->max_addresses) == 0) sx->max_rcpt = 999999;
+sx->igquotstr = US"";
+if (!sx->helo_data) sx->helo_data = ob->helo_data;
+
+smtp_command = US"initial connection";
+
+/* Set up the buffer for reading SMTP response packets. */
+
+sx->inblock.buffer = sx->inbuffer;
+sx->inblock.buffersize = sizeof(sx->inbuffer);
+sx->inblock.ptr = sx->inbuffer;
+sx->inblock.ptrend = sx->inbuffer;
+
+/* Set up the buffer for holding SMTP commands while pipelining */
+
+sx->outblock.buffer = sx->outbuffer;
+sx->outblock.buffersize = sizeof(sx->outbuffer);
+sx->outblock.ptr = sx->outbuffer;
+
+/* Reset the parameters of a TLS session. */
+
+tls_out.bits = 0;
+tls_out.cipher = NULL; /* the one we may use for this transport */
+tls_out.ourcert = NULL;
+tls_out.peercert = NULL;
+tls_out.peerdn = NULL;
+#ifdef USE_OPENSSL
+tls_out.sni = NULL;
+#endif
+tls_out.ocsp = OCSP_NOT_REQ;
+#ifndef DISABLE_TLS_RESUME
+tls_out.resumption = 0;
+#endif
+tls_out.ver = NULL;
+
+/* Flip the legacy TLS-related variables over to the outbound set in case
+they're used in the context of the transport. Don't bother resetting
+afterward (when being used by a transport) as we're in a subprocess.
+For verify, unflipped once the callout is dealt with */
+
+tls_modify_variables(&tls_out);
+
+#ifdef DISABLE_TLS
+if (sx->smtps)
+ {
+ set_errno_nohost(sx->addrlist, ERRNO_TLSFAILURE, US"TLS support not available",
+ DEFER, FALSE, &sx->delivery_start);
+ return ERROR;
+ }
+#else
+
+/* If we have a proxied TLS connection, check usability for this message */
+
+if (continue_hostname && continue_proxy_cipher)
+ {
+ int rc;
+ const uschar * sni = US"";
+
+# ifdef SUPPORT_DANE
+ /* Check if the message will be DANE-verified; if so force TLS and its SNI */
+
+ tls_out.dane_verified = FALSE;
+ smtp_port_for_connect(sx->conn_args.host, sx->port);
+ if ( sx->conn_args.host->dnssec == DS_YES
+ && (rc = check_force_dane_conn(sx, ob)) != OK
+ )
+ return rc;
+# endif
+
+ /* If the SNI or the DANE status required for the new message differs from the
+ existing conn drop the connection to force a new one. */
+
+ if (ob->tls_sni && !(sni = expand_cstring(ob->tls_sni)))
+ log_write(0, LOG_MAIN|LOG_PANIC,
+ "<%s>: failed to expand transport's tls_sni value: %s",
+ sx->addrlist->address, expand_string_message);
+
+# ifdef SUPPORT_DANE
+ if ( (continue_proxy_sni ? (Ustrcmp(continue_proxy_sni, sni) == 0) : !*sni)
+ && continue_proxy_dane == sx->conn_args.dane)
+ {
+ tls_out.sni = US sni;
+ if ((tls_out.dane_verified = continue_proxy_dane))
+ sx->conn_args.host->dnssec = DS_YES;
+ }
+# else
+ if ((continue_proxy_sni ? (Ustrcmp(continue_proxy_sni, sni) == 0) : !*sni))
+ tls_out.sni = US sni;
+# endif
+ else
+ {
+ DEBUG(D_transport)
+ debug_printf("Closing proxied-TLS connection due to SNI mismatch\n");
+
+ smtp_debug_cmd(US"QUIT", 0);
+ write(0, "QUIT\r\n", 6);
+ close(0);
+ continue_hostname = continue_proxy_cipher = NULL;
+ f.continue_more = FALSE;
+ continue_sequence = 1; /* Unfortunately, this process cannot affect success log
+ which is done by delivery proc. Would have to pass this
+ back through reporting pipe. */
+ }
+ }
+#endif /*!DISABLE_TLS*/
+
+/* Make a connection to the host if this isn't a continued delivery, and handle
+the initial interaction and HELO/EHLO/LHLO. Connect timeout errors are handled
+specially so they can be identified for retries. */
+
+if (!continue_hostname)
+ {
+ if (sx->verify)
+ HDEBUG(D_verify) debug_printf("interface=%s port=%d\n", sx->conn_args.interface, sx->port);
+
+ /* Arrange to report to calling process this is a new connection */
+
+ clearflag(sx->first_addr, af_cont_conn);
+ setflag(sx->first_addr, af_new_conn);
+
+ /* Get the actual port the connection will use, into sx->conn_args.host */
+
+ smtp_port_for_connect(sx->conn_args.host, sx->port);
+
+#ifdef SUPPORT_DANE
+ /* Do TLSA lookup for DANE */
+ {
+ tls_out.dane_verified = FALSE;
+ tls_out.tlsa_usage = 0;
+
+ if (sx->conn_args.host->dnssec == DS_YES)
+ {
+ int rc;
+ if ((rc = check_force_dane_conn(sx, ob)) != OK)
+ return rc;
+ }
+ else if (sx->dane_required)
+ {
+ set_errno_nohost(sx->addrlist, ERRNO_DNSDEFER,
+ string_sprintf("DANE error: %s lookup not DNSSEC", sx->conn_args.host->name),
+ FAIL, FALSE, &sx->delivery_start);
+# ifndef DISABLE_EVENT
+ (void) event_raise(sx->conn_args.tblock->event_action,
+ US"dane:fail", US"dane-required", NULL);
+# endif
+ return FAIL;
+ }
+ }
+#endif /*DANE*/
+
+ /* Make the TCP connection */
+
+ sx->cctx.tls_ctx = NULL;
+ sx->inblock.cctx = sx->outblock.cctx = &sx->cctx;
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+ sx->peer_limit_mail = sx->peer_limit_rcpt = sx->peer_limit_rcptdom =
+#endif
+ sx->avoid_option = sx->peer_offered = smtp_peer_options = 0;
+#ifndef DISABLE_CLIENT_CMD_LOG
+ client_cmd_log = NULL;
+#endif
+
+#ifndef DISABLE_PIPE_CONNECT
+ if ( verify_check_given_host(CUSS &ob->hosts_pipe_connect,
+ sx->conn_args.host) == OK)
+
+ /* We don't find out the local ip address until the connect, so if
+ the helo string might use it avoid doing early-pipelining. */
+
+ if ( !sx->helo_data
+ || sx->conn_args.interface
+ || !Ustrstr(sx->helo_data, "$sending_ip_address")
+ || Ustrstr(sx->helo_data, "def:sending_ip_address")
+ )
+ {
+ sx->early_pipe_ok = TRUE;
+ if ( read_ehlo_cache_entry(sx)
+ && sx->ehlo_resp.cleartext_features & OPTION_EARLY_PIPE)
+ {
+ DEBUG(D_transport)
+ debug_printf("Using cached cleartext PIPECONNECT\n");
+ sx->early_pipe_active = TRUE;
+ sx->peer_offered = sx->ehlo_resp.cleartext_features;
+ }
+ }
+ else DEBUG(D_transport)
+ debug_printf("helo needs $sending_ip_address; avoid early-pipelining\n");
+
+PIPE_CONNECT_RETRY:
+ if (sx->early_pipe_active)
+ {
+ sx->outblock.conn_args = &sx->conn_args;
+ (void) smtp_boundsock(&sx->conn_args);
+ }
+ else
+#endif
+ {
+ blob lazy_conn = {.data = NULL};
+ /* For TLS-connect, a TFO lazy-connect is useful since the Client Hello
+ can go on the TCP SYN. */
+
+ if ((sx->cctx.sock = smtp_connect(&sx->conn_args,
+ sx->smtps ? &lazy_conn : NULL)) < 0)
+ {
+ set_errno_nohost(sx->addrlist,
+ errno == ETIMEDOUT ? ERRNO_CONNECTTIMEOUT : errno,
+ sx->verify ? US strerror(errno) : NULL,
+ DEFER, FALSE, &sx->delivery_start);
+ sx->send_quit = FALSE;
+ return DEFER;
+ }
+#ifdef TCP_QUICKACK
+ (void) setsockopt(sx->cctx.sock, IPPROTO_TCP, TCP_QUICKACK, US &off,
+ sizeof(off));
+#endif
+ }
+ /* Expand the greeting message while waiting for the initial response. (Makes
+ sense if helo_data contains ${lookup dnsdb ...} stuff). The expansion is
+ delayed till here so that $sending_ip_address and $sending_port are set.
+ Those will be known even for a TFO lazy-connect, having been set by the bind().
+ For early-pipe, we are ok if binding to a local interface; otherwise (if
+ $sending_ip_address is seen in helo_data) we disabled early-pipe above. */
+
+ if (sx->helo_data)
+ if (!(sx->helo_data = expand_string(sx->helo_data)))
+ if (sx->verify)
+ log_write(0, LOG_MAIN|LOG_PANIC,
+ "<%s>: failed to expand transport's helo_data value for callout: %s",
+ sx->addrlist->address, expand_string_message);
+
+#ifdef SUPPORT_I18N
+ if (sx->helo_data)
+ {
+ expand_string_message = NULL;
+ if ((sx->helo_data = string_domain_utf8_to_alabel(sx->helo_data,
+ &expand_string_message)),
+ expand_string_message)
+ if (sx->verify)
+ log_write(0, LOG_MAIN|LOG_PANIC,
+ "<%s>: failed to expand transport's helo_data value for callout: %s",
+ sx->addrlist->address, expand_string_message);
+ else
+ sx->helo_data = NULL;
+ }
+#endif
+
+ /* The first thing is to wait for an initial OK response. The dreaded "goto"
+ is nevertheless a reasonably clean way of programming this kind of logic,
+ where you want to escape on any error. */
+
+ if (!sx->smtps)
+ {
+#ifndef DISABLE_PIPE_CONNECT
+ if (sx->early_pipe_active)
+ {
+ sx->pending_BANNER = TRUE; /* sync_responses() must eventually handle */
+ sx->outblock.cmd_count = 1;
+ }
+ else
+#endif
+ {
+ if (!smtp_reap_banner(sx))
+ goto RESPONSE_FAILED;
+ }
+
+#ifndef DISABLE_EVENT
+ {
+ uschar * s;
+ lookup_dnssec_authenticated = sx->conn_args.host->dnssec==DS_YES ? US"yes"
+ : sx->conn_args.host->dnssec==DS_NO ? US"no" : NULL;
+ s = event_raise(sx->conn_args.tblock->event_action, US"smtp:connect", sx->buffer, NULL);
+ if (s)
+ {
+ set_errno_nohost(sx->addrlist, ERRNO_EXPANDFAIL,
+ string_sprintf("deferred by smtp:connect event expansion: %s", s),
+ DEFER, FALSE, &sx->delivery_start);
+ yield = DEFER;
+ goto SEND_QUIT;
+ }
+ }
+#endif
+
+ /* Now check if the helo_data expansion went well, and sign off cleanly if
+ it didn't. */
+
+ if (!sx->helo_data)
+ {
+ message = string_sprintf("failed to expand helo_data: %s",
+ expand_string_message);
+ set_errno_nohost(sx->addrlist, ERRNO_EXPANDFAIL, message, DEFER, FALSE, &sx->delivery_start);
+ yield = DEFER;
+ goto SEND_QUIT;
+ }
+ }
+
+/** Debugging without sending a message
+sx->addrlist->transport_return = DEFER;
+goto SEND_QUIT;
+**/
+
+ /* Errors that occur after this point follow an SMTP command, which is
+ left in big_buffer by smtp_write_command() for use in error messages. */
+
+ smtp_command = big_buffer;
+
+ /* Tell the remote who we are...
+
+ February 1998: A convention has evolved that ESMTP-speaking MTAs include the
+ string "ESMTP" in their greeting lines, so make Exim send EHLO if the
+ greeting is of this form. The assumption was that the far end supports it
+ properly... but experience shows that there are some that give 5xx responses,
+ even though the banner includes "ESMTP" (there's a bloody-minded one that
+ says "ESMTP not spoken here"). Cope with that case.
+
+ September 2000: Time has passed, and it seems reasonable now to always send
+ EHLO at the start. It is also convenient to make the change while installing
+ the TLS stuff.
+
+ July 2003: Joachim Wieland met a broken server that advertises "PIPELINING"
+ but times out after sending MAIL FROM, RCPT TO and DATA all together. There
+ would be no way to send out the mails, so there is now a host list
+ "hosts_avoid_esmtp" that disables ESMTP for special hosts and solves the
+ PIPELINING problem as well. Maybe it can also be useful to cure other
+ problems with broken servers.
+
+ Exim originally sent "Helo" at this point and ran for nearly a year that way.
+ Then somebody tried it with a Microsoft mailer... It seems that all other
+ mailers use upper case for some reason (the RFC is quite clear about case
+ independence) so, for peace of mind, I gave in. */
+
+ sx->esmtp = verify_check_given_host(CUSS &ob->hosts_avoid_esmtp, sx->conn_args.host) != OK;
+
+ /* Alas; be careful, since this goto is not an error-out, so conceivably
+ we might set data between here and the target which we assume to exist
+ and be usable. I can see this coming back to bite us. */
+#ifndef DISABLE_TLS
+ if (sx->smtps)
+ {
+ smtp_peer_options |= OPTION_TLS;
+ suppress_tls = FALSE;
+ ob->tls_tempfail_tryclear = FALSE;
+ smtp_command = US"SSL-on-connect";
+ goto TLS_NEGOTIATE;
+ }
+#endif
+
+ if (sx->esmtp)
+ {
+ if (smtp_write_command(sx,
+#ifndef DISABLE_PIPE_CONNECT
+ sx->early_pipe_active ? SCMD_BUFFER :
+#endif
+ SCMD_FLUSH,
+ "%s %s\r\n", sx->lmtp ? "LHLO" : "EHLO", sx->helo_data) < 0)
+ goto SEND_FAILED;
+ sx->esmtp_sent = TRUE;
+
+#ifndef DISABLE_PIPE_CONNECT
+ if (sx->early_pipe_active)
+ {
+ sx->pending_EHLO = TRUE;
+
+ /* If we have too many authenticators to handle and might need to AUTH
+ for this transport, pipeline no further as we will need the
+ list of auth methods offered. Reap the banner and EHLO. */
+
+ if ( (ob->hosts_require_auth || ob->hosts_try_auth)
+ && f.smtp_in_early_pipe_no_auth)
+ {
+ DEBUG(D_transport) debug_printf("may need to auth, so pipeline no further\n");
+ if (smtp_write_command(sx, SCMD_FLUSH, NULL) < 0)
+ goto SEND_FAILED;
+ if (sync_responses(sx, 2, 0) != 0)
+ {
+ HDEBUG(D_transport)
+ debug_printf("failed reaping pipelined cmd responses\n");
+ goto RESPONSE_FAILED;
+ }
+ sx->early_pipe_active = FALSE;
+ }
+ }
+ else
+#endif
+ if (!smtp_reap_ehlo(sx))
+ goto RESPONSE_FAILED;
+ }
+ else
+ DEBUG(D_transport)
+ debug_printf("not sending EHLO (host matches hosts_avoid_esmtp)\n");
+
+#ifndef DISABLE_PIPE_CONNECT
+ if (!sx->early_pipe_active)
+#endif
+ if (!sx->esmtp)
+ {
+ BOOL good_response;
+ int n = sizeof(sx->buffer);
+ uschar * rsp = sx->buffer;
+
+ if (sx->esmtp_sent && (n = Ustrlen(sx->buffer) + 1) < sizeof(sx->buffer)/2)
+ { rsp = sx->buffer + n; n = sizeof(sx->buffer) - n; }
+
+ if (smtp_write_command(sx, SCMD_FLUSH, "HELO %s\r\n", sx->helo_data) < 0)
+ goto SEND_FAILED;
+ good_response = smtp_read_response(sx, rsp, n, '2', ob->command_timeout);
+#ifdef EXPERIMENTAL_DSN_INFO
+ sx->helo_response = string_copy(rsp);
+#endif
+ if (!good_response)
+ {
+ /* Handle special logging for a closed connection after HELO
+ when had previously sent EHLO */
+
+ if (rsp != sx->buffer && rsp[0] == 0 && (errno == 0 || errno == ECONNRESET))
+ {
+ errno = ERRNO_SMTPCLOSED;
+ goto EHLOHELO_FAILED;
+ }
+ memmove(sx->buffer, rsp, Ustrlen(rsp));
+ goto RESPONSE_FAILED;
+ }
+ }
+
+ if (sx->esmtp || sx->lmtp)
+ {
+#ifndef DISABLE_PIPE_CONNECT
+ if (!sx->early_pipe_active)
+#endif
+ {
+ sx->peer_offered = ehlo_response(sx->buffer,
+ OPTION_TLS /* others checked later */
+#ifndef DISABLE_PIPE_CONNECT
+ | (sx->early_pipe_ok
+ ? OPTION_IGNQ
+ | OPTION_CHUNKING | OPTION_PRDR | OPTION_DSN | OPTION_PIPE | OPTION_SIZE
+#ifdef SUPPORT_I18N
+ | OPTION_UTF8
+#endif
+ | OPTION_EARLY_PIPE
+ : 0
+ )
+#endif
+ );
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+ if (tls_out.active.sock >= 0 || !(sx->peer_offered & OPTION_TLS))
+ {
+ ehlo_response_limits_read(sx);
+ ehlo_response_limits_apply(sx);
+ }
+#endif
+#ifndef DISABLE_PIPE_CONNECT
+ if (sx->early_pipe_ok)
+ {
+ sx->ehlo_resp.cleartext_features = sx->peer_offered;
+
+ if ( (sx->peer_offered & (OPTION_PIPE | OPTION_EARLY_PIPE))
+ == (OPTION_PIPE | OPTION_EARLY_PIPE))
+ {
+ DEBUG(D_transport) debug_printf("PIPECONNECT usable in future for this IP\n");
+ sx->ehlo_resp.cleartext_auths = study_ehlo_auths(sx);
+ write_ehlo_cache_entry(sx);
+ }
+ }
+#endif
+ ehlo_response_lbserver(sx, ob);
+ }
+
+ /* Set tls_offered if the response to EHLO specifies support for STARTTLS. */
+
+#ifndef DISABLE_TLS
+ smtp_peer_options |= sx->peer_offered & OPTION_TLS;
+#endif
+ }
+ }
+
+/* For continuing deliveries down the same channel, having re-exec'd the socket
+is the standard input; for a socket held open from verify it is recorded
+in the cutthrough context block. Either way we don't need to redo EHLO here
+(but may need to do so for TLS - see below).
+Set up the pointer to where subsequent commands will be left, for
+error messages. Note that smtp_peer_options will have been
+set from the command line if they were set in the process that passed the
+connection on. */
+
+/*XXX continue case needs to propagate DSN_INFO, prob. in deliver.c
+as the continue goes via transport_pass_socket() and doublefork and exec.
+It does not wait. Unclear how we keep separate host's responses
+separate - we could match up by host ip+port as a bodge. */
+
+else
+ {
+ if (cutthrough.cctx.sock >= 0 && cutthrough.callout_hold_only)
+ {
+ sx->cctx = cutthrough.cctx;
+ sx->conn_args.host->port = sx->port = cutthrough.host.port;
+ }
+ else
+ {
+ sx->cctx.sock = 0; /* stdin */
+ sx->cctx.tls_ctx = NULL;
+ smtp_port_for_connect(sx->conn_args.host, sx->port); /* Record the port that was used */
+ }
+ sx->inblock.cctx = sx->outblock.cctx = &sx->cctx;
+ smtp_command = big_buffer;
+ sx->peer_offered = smtp_peer_options;
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+ /* Limits passed by cmdline over exec. */
+ ehlo_limits_apply(sx,
+ sx->peer_limit_mail = continue_limit_mail,
+ sx->peer_limit_rcpt = continue_limit_rcpt,
+ sx->peer_limit_rcptdom = continue_limit_rcptdom);
+#endif
+ sx->helo_data = NULL; /* ensure we re-expand ob->helo_data */
+
+ /* For a continued connection with TLS being proxied for us, or a
+ held-open verify connection with TLS, nothing more to do. */
+
+ if ( continue_proxy_cipher
+ || (cutthrough.cctx.sock >= 0 && cutthrough.callout_hold_only
+ && cutthrough.is_tls)
+ )
+ {
+ sx->pipelining_used = pipelining_active = !!(smtp_peer_options & OPTION_PIPE);
+ HDEBUG(D_transport) debug_printf("continued connection, %s TLS\n",
+ continue_proxy_cipher ? "proxied" : "verify conn with");
+ return OK;
+ }
+ HDEBUG(D_transport) debug_printf("continued connection, no TLS\n");
+ }
+
+/* If TLS is available on this connection, whether continued or not, attempt to
+start up a TLS session, unless the host is in hosts_avoid_tls. If successful,
+send another EHLO - the server may give a different answer in secure mode. We
+use a separate buffer for reading the response to STARTTLS so that if it is
+negative, the original EHLO data is available for subsequent analysis, should
+the client not be required to use TLS. If the response is bad, copy the buffer
+for error analysis. */
+
+#ifndef DISABLE_TLS
+if ( smtp_peer_options & OPTION_TLS
+ && !suppress_tls
+ && verify_check_given_host(CUSS &ob->hosts_avoid_tls, sx->conn_args.host) != OK
+ && ( !sx->verify
+ || verify_check_given_host(CUSS &ob->hosts_verify_avoid_tls, sx->conn_args.host) != OK
+ ) )
+ {
+ uschar buffer2[4096];
+
+ if (smtp_write_command(sx, SCMD_FLUSH, "STARTTLS\r\n") < 0)
+ goto SEND_FAILED;
+
+#ifndef DISABLE_PIPE_CONNECT
+ /* If doing early-pipelining reap the banner and EHLO-response but leave
+ the response for the STARTTLS we just sent alone. On fail, assume wrong
+ cached capability and retry with the pipelining disabled. */
+
+ if (sx->early_pipe_active)
+ {
+ if (sync_responses(sx, 2, 0) != 0)
+ {
+ HDEBUG(D_transport)
+ debug_printf("failed reaping pipelined cmd responses\n");
+ close(sx->cctx.sock);
+ sx->cctx.sock = -1;
+ sx->early_pipe_active = FALSE;
+ goto PIPE_CONNECT_RETRY;
+ }
+ }
+#endif
+
+ /* If there is an I/O error, transmission of this message is deferred. If
+ there is a temporary rejection of STARRTLS and tls_tempfail_tryclear is
+ false, we also defer. However, if there is a temporary rejection of STARTTLS
+ and tls_tempfail_tryclear is true, or if there is an outright rejection of
+ STARTTLS, we carry on. This means we will try to send the message in clear,
+ unless the host is in hosts_require_tls (tested below). */
+
+ if (!smtp_read_response(sx, buffer2, sizeof(buffer2), '2', ob->command_timeout))
+ {
+ if ( errno != 0
+ || buffer2[0] == 0
+ || (buffer2[0] == '4' && !ob->tls_tempfail_tryclear)
+ )
+ {
+ Ustrncpy(sx->buffer, buffer2, sizeof(sx->buffer));
+ sx->buffer[sizeof(sx->buffer)-1] = '\0';
+ goto RESPONSE_FAILED;
+ }
+ }
+
+ /* STARTTLS accepted: try to negotiate a TLS session. */
+
+ else
+ TLS_NEGOTIATE:
+ {
+ sx->conn_args.sending_ip_address = sending_ip_address;
+ if (!tls_client_start(&sx->cctx, &sx->conn_args, sx->addrlist, &tls_out, &tls_errstr))
+ {
+ /* TLS negotiation failed; give an error. From outside, this function may
+ be called again to try in clear on a new connection, if the options permit
+ it for this host. */
+ TLS_CONN_FAILED:
+ DEBUG(D_tls) debug_printf("TLS session fail: %s\n", tls_errstr);
+
+# ifdef SUPPORT_DANE
+ if (sx->conn_args.dane)
+ {
+ log_write(0, LOG_MAIN,
+ "DANE attempt failed; TLS connection to %s [%s]: %s",
+ sx->conn_args.host->name, sx->conn_args.host->address, tls_errstr);
+# ifndef DISABLE_EVENT
+ (void) event_raise(sx->conn_args.tblock->event_action,
+ US"dane:fail", US"validation-failure", NULL); /* could do with better detail */
+# endif
+ }
+# endif
+
+ errno = ERRNO_TLSFAILURE;
+ message = string_sprintf("TLS session: %s", tls_errstr);
+ sx->send_quit = FALSE;
+ goto TLS_FAILED;
+ }
+ sx->send_tlsclose = TRUE;
+
+ /* TLS session is set up. Check the inblock fill level. If there is
+ content then as we have not yet done a tls read it must have arrived before
+ the TLS handshake, in-clear. That violates the sync requirement of the
+ STARTTLS RFC, so fail. */
+
+ if (sx->inblock.ptr != sx->inblock.ptrend)
+ {
+ DEBUG(D_tls)
+ {
+ int i = sx->inblock.ptrend - sx->inblock.ptr;
+ debug_printf("unused data in input buffer after ack for STARTTLS:\n"
+ "'%.*s'%s\n",
+ i > 100 ? 100 : i, sx->inblock.ptr, i > 100 ? "..." : "");
+ }
+ tls_errstr = US"synch error before connect";
+ goto TLS_CONN_FAILED;
+ }
+
+ smtp_peer_options_wrap = smtp_peer_options;
+ for (address_item * addr = sx->addrlist; addr; addr = addr->next)
+ if (addr->transport_return == PENDING_DEFER)
+ {
+ addr->cipher = tls_out.cipher;
+ addr->ourcert = tls_out.ourcert;
+ addr->peercert = tls_out.peercert;
+ addr->peerdn = tls_out.peerdn;
+ addr->ocsp = tls_out.ocsp;
+ addr->tlsver = tls_out.ver;
+ }
+ }
+ }
+
+/* if smtps, we'll have smtp_command set to something else; always safe to
+reset it here. */
+smtp_command = big_buffer;
+
+/* If we started TLS, redo the EHLO/LHLO exchange over the secure channel. If
+helo_data is null, we are dealing with a connection that was passed from
+another process, and so we won't have expanded helo_data above. We have to
+expand it here. $sending_ip_address and $sending_port are set up right at the
+start of the Exim process (in exim.c). */
+
+if (tls_out.active.sock >= 0)
+ {
+ uschar * greeting_cmd;
+
+ if (!sx->helo_data && !(sx->helo_data = expand_string(ob->helo_data)))
+ {
+ uschar *message = string_sprintf("failed to expand helo_data: %s",
+ expand_string_message);
+ set_errno_nohost(sx->addrlist, ERRNO_EXPANDFAIL, message, DEFER, FALSE, &sx->delivery_start);
+ yield = DEFER;
+ goto SEND_QUIT;
+ }
+
+#ifndef DISABLE_PIPE_CONNECT
+ /* For SMTPS there is no cleartext early-pipe; use the crypted permission bit.
+ We're unlikely to get the group sent and delivered before the server sends its
+ banner, but it's still worth sending as a group.
+ For STARTTLS allow for cleartext early-pipe but no crypted early-pipe, but not
+ the reverse. */
+
+ if (sx->smtps ? sx->early_pipe_ok : sx->early_pipe_active)
+ {
+ sx->peer_offered = sx->ehlo_resp.crypted_features;
+ if ((sx->early_pipe_active =
+ !!(sx->ehlo_resp.crypted_features & OPTION_EARLY_PIPE)))
+ DEBUG(D_transport) debug_printf("Using cached crypted PIPECONNECT\n");
+ }
+#endif
+#ifdef EXPERIMMENTAL_ESMTP_LIMITS
+ /* As we are about to send another EHLO, forget any LIMITS received so far. */
+ sx->peer_limit_mail = sx->peer_limit_rcpt = sx->peer_limit_rcptdom = 0;
+ if ((sx->max_mail = sx->conn_args.tblock->connection_max_message) == 0) sx->max_mail = 999999;
+ if ((sx->max_rcpt = sx->conn_args.tblock->max_addresses) == 0) sx->max_rcpt = 999999;
+ sx->single_rcpt_domain = FALSE;
+#endif
+
+ /* For SMTPS we need to wait for the initial OK response. */
+ if (sx->smtps)
+#ifndef DISABLE_PIPE_CONNECT
+ if (sx->early_pipe_active)
+ {
+ sx->pending_BANNER = TRUE;
+ sx->outblock.cmd_count = 1;
+ }
+ else
+#endif
+ if (!smtp_reap_banner(sx))
+ goto RESPONSE_FAILED;
+
+ if (sx->lmtp)
+ greeting_cmd = US"LHLO";
+ else if (sx->esmtp)
+ greeting_cmd = US"EHLO";
+ else
+ {
+ greeting_cmd = US"HELO";
+ DEBUG(D_transport)
+ debug_printf("not sending EHLO (host matches hosts_avoid_esmtp)\n");
+ }
+
+ if (smtp_write_command(sx,
+#ifndef DISABLE_PIPE_CONNECT
+ sx->early_pipe_active ? SCMD_BUFFER :
+#endif
+ SCMD_FLUSH,
+ "%s %s\r\n", greeting_cmd, sx->helo_data) < 0)
+ goto SEND_FAILED;
+
+#ifndef DISABLE_PIPE_CONNECT
+ if (sx->early_pipe_active)
+ sx->pending_EHLO = TRUE;
+ else
+#endif
+ {
+ if (!smtp_reap_ehlo(sx))
+#ifdef USE_GNUTLS
+ {
+ /* The GnuTLS layer in Exim only spots a server-rejection of a client
+ cert late, under TLS1.3 - which means here; the first time we try to
+ receive crypted data. Treat it as if it was a connect-time failure.
+ See also the early-pipe equivalent... which will be hard; every call
+ to sync_responses will need to check the result.
+ It would be nicer to have GnuTLS check the cert during the handshake.
+ Can it do that, with all the flexibility we need? */
+
+ tls_errstr = US"error on first read";
+ goto TLS_CONN_FAILED;
+ }
+#else
+ goto RESPONSE_FAILED;
+#endif
+ smtp_peer_options = 0;
+ }
+ }
+
+/* If the host is required to use a secure channel, ensure that we
+have one. */
+
+else if ( sx->smtps
+# ifdef SUPPORT_DANE
+ || sx->conn_args.dane
+# endif
+ || verify_check_given_host(CUSS &ob->hosts_require_tls, sx->conn_args.host) == OK
+ )
+ {
+ errno = ERRNO_TLSREQUIRED;
+ message = string_sprintf("a TLS session is required, but %s",
+ smtp_peer_options & OPTION_TLS
+ ? "an attempt to start TLS failed" : "the server did not offer TLS support");
+# if defined(SUPPORT_DANE) && !defined(DISABLE_EVENT)
+ if (sx->conn_args.dane)
+ (void) event_raise(sx->conn_args.tblock->event_action, US"dane:fail",
+ smtp_peer_options & OPTION_TLS
+ ? US"validation-failure" /* could do with better detail */
+ : US"starttls-not-supported",
+ NULL);
+# endif
+ goto TLS_FAILED;
+ }
+#endif /*DISABLE_TLS*/
+
+/* If TLS is active, we have just started it up and re-done the EHLO command,
+so its response needs to be analyzed. If TLS is not active and this is a
+continued session down a previously-used socket, we haven't just done EHLO, so
+we skip this. */
+
+if ( !continue_hostname
+#ifndef DISABLE_TLS
+ || tls_out.active.sock >= 0
+#endif
+ )
+ {
+ if (sx->esmtp || sx->lmtp)
+ {
+#ifndef DISABLE_PIPE_CONNECT
+ if (!sx->early_pipe_active)
+#endif
+ {
+ sx->peer_offered = ehlo_response(sx->buffer,
+ 0 /* no TLS */
+#ifndef DISABLE_PIPE_CONNECT
+ | (sx->lmtp && ob->lmtp_ignore_quota ? OPTION_IGNQ : 0)
+ | OPTION_DSN | OPTION_PIPE | OPTION_SIZE
+ | OPTION_CHUNKING | OPTION_PRDR | OPTION_UTF8
+ | (tls_out.active.sock >= 0 ? OPTION_EARLY_PIPE : 0) /* not for lmtp */
+
+#else
+
+ | (sx->lmtp && ob->lmtp_ignore_quota ? OPTION_IGNQ : 0)
+ | OPTION_CHUNKING
+ | OPTION_PRDR
+# ifdef SUPPORT_I18N
+ | (sx->addrlist->prop.utf8_msg ? OPTION_UTF8 : 0)
+ /*XXX if we hand peercaps on to continued-conn processes,
+ must not depend on this addr */
+# endif
+ | OPTION_DSN
+ | OPTION_PIPE
+ | (ob->size_addition >= 0 ? OPTION_SIZE : 0)
+#endif
+ );
+#ifndef DISABLE_PIPE_CONNECT
+ if (tls_out.active.sock >= 0)
+ sx->ehlo_resp.crypted_features = sx->peer_offered;
+#endif
+
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+ if (tls_out.active.sock >= 0 || !(sx->peer_offered & OPTION_TLS))
+ {
+ ehlo_response_limits_read(sx);
+ ehlo_response_limits_apply(sx);
+ }
+#endif
+ }
+
+ /* Set for IGNOREQUOTA if the response to LHLO specifies support and the
+ lmtp_ignore_quota option was set. */
+
+ sx->igquotstr = sx->peer_offered & OPTION_IGNQ ? US" IGNOREQUOTA" : US"";
+
+ /* If the response to EHLO specified support for the SIZE parameter, note
+ this, provided size_addition is non-negative. */
+
+ smtp_peer_options |= sx->peer_offered & OPTION_SIZE;
+
+ /* Note whether the server supports PIPELINING. If hosts_avoid_esmtp matched
+ the current host, esmtp will be false, so PIPELINING can never be used. If
+ the current host matches hosts_avoid_pipelining, don't do it. */
+
+ if ( sx->peer_offered & OPTION_PIPE
+ && verify_check_given_host(CUSS &ob->hosts_avoid_pipelining, sx->conn_args.host) != OK)
+ smtp_peer_options |= OPTION_PIPE;
+
+ DEBUG(D_transport) debug_printf("%susing PIPELINING\n",
+ smtp_peer_options & OPTION_PIPE ? "" : "not ");
+
+ if ( sx->peer_offered & OPTION_CHUNKING
+ && verify_check_given_host(CUSS &ob->hosts_try_chunking, sx->conn_args.host) == OK)
+ smtp_peer_options |= OPTION_CHUNKING;
+
+ if (smtp_peer_options & OPTION_CHUNKING)
+ DEBUG(D_transport) debug_printf("CHUNKING usable\n");
+
+#ifndef DISABLE_PRDR
+ if ( sx->peer_offered & OPTION_PRDR
+ && verify_check_given_host(CUSS &ob->hosts_try_prdr, sx->conn_args.host) == OK)
+ smtp_peer_options |= OPTION_PRDR;
+
+ if (smtp_peer_options & OPTION_PRDR)
+ DEBUG(D_transport) debug_printf("PRDR usable\n");
+#endif
+
+ /* Note if the server supports DSN */
+ smtp_peer_options |= sx->peer_offered & OPTION_DSN;
+ DEBUG(D_transport) debug_printf("%susing DSN\n",
+ sx->peer_offered & OPTION_DSN ? "" : "not ");
+
+#ifndef DISABLE_PIPE_CONNECT
+ if ( sx->early_pipe_ok
+ && !sx->early_pipe_active
+ && tls_out.active.sock >= 0
+ && smtp_peer_options & OPTION_PIPE
+ && ( sx->ehlo_resp.cleartext_features | sx->ehlo_resp.crypted_features)
+ & OPTION_EARLY_PIPE)
+ {
+ DEBUG(D_transport) debug_printf("PIPECONNECT usable in future for this IP\n");
+ sx->ehlo_resp.crypted_auths = study_ehlo_auths(sx);
+ write_ehlo_cache_entry(sx);
+ }
+#endif
+
+ /* Note if the response to EHLO specifies support for the AUTH extension.
+ If it has, check that this host is one we want to authenticate to, and do
+ the business. The host name and address must be available when the
+ authenticator's client driver is running. */
+
+ switch (yield = smtp_auth(sx))
+ {
+ default: goto SEND_QUIT;
+ case OK: break;
+ case FAIL_SEND: goto SEND_FAILED;
+ case FAIL: goto RESPONSE_FAILED;
+ }
+ }
+ }
+sx->pipelining_used = pipelining_active = !!(smtp_peer_options & OPTION_PIPE);
+
+/* The setting up of the SMTP call is now complete. Any subsequent errors are
+message-specific. */
+
+sx->setting_up = FALSE;
+
+#ifdef SUPPORT_I18N
+if (sx->addrlist->prop.utf8_msg)
+ {
+ uschar * s;
+
+ /* If the transport sets a downconversion mode it overrides any set by ACL
+ for the message. */
+
+ if ((s = ob->utf8_downconvert))
+ {
+ if (!(s = expand_string(s)))
+ {
+ message = string_sprintf("failed to expand utf8_downconvert: %s",
+ expand_string_message);
+ set_errno_nohost(sx->addrlist, ERRNO_EXPANDFAIL, message, DEFER, FALSE, &sx->delivery_start);
+ yield = DEFER;
+ goto SEND_QUIT;
+ }
+ switch (*s)
+ {
+ case '1': sx->addrlist->prop.utf8_downcvt = TRUE;
+ sx->addrlist->prop.utf8_downcvt_maybe = FALSE;
+ break;
+ case '0': sx->addrlist->prop.utf8_downcvt = FALSE;
+ sx->addrlist->prop.utf8_downcvt_maybe = FALSE;
+ break;
+ case '-': if (s[1] == '1')
+ {
+ sx->addrlist->prop.utf8_downcvt = FALSE;
+ sx->addrlist->prop.utf8_downcvt_maybe = TRUE;
+ }
+ break;
+ }
+ }
+
+ sx->utf8_needed = !sx->addrlist->prop.utf8_downcvt
+ && !sx->addrlist->prop.utf8_downcvt_maybe;
+ DEBUG(D_transport) if (!sx->utf8_needed)
+ debug_printf("utf8: %s downconvert\n",
+ sx->addrlist->prop.utf8_downcvt ? "mandatory" : "optional");
+ }
+
+/* If this is an international message we need the host to speak SMTPUTF8 */
+if (sx->utf8_needed && !(sx->peer_offered & OPTION_UTF8))
+ {
+ errno = ERRNO_UTF8_FWD;
+ goto RESPONSE_FAILED;
+ }
+#endif /*SUPPORT_I18N*/
+
+return OK;
+
+
+ {
+ int code;
+
+ RESPONSE_FAILED:
+ if (errno == ECONNREFUSED) /* first-read error on a TFO conn */
+ {
+ /* There is a testing facility for simulating a connection timeout, as I
+ can't think of any other way of doing this. It converts a connection
+ refused into a timeout if the timeout is set to 999999. This is done for
+ a 3whs connection in ip_connect(), but a TFO connection does not error
+ there - instead it gets ECONNREFUSED on the first data read. Tracking
+ that a TFO really was done is too hard, or we would set a
+ sx->pending_conn_done bit and test that in smtp_reap_banner() and
+ smtp_reap_ehlo(). That would let us also add the conn-timeout to the
+ cmd-timeout. */
+
+ if (f.running_in_test_harness && ob->connect_timeout == 999999)
+ errno = ETIMEDOUT;
+ set_errno_nohost(sx->addrlist,
+ errno == ETIMEDOUT ? ERRNO_CONNECTTIMEOUT : errno,
+ sx->verify ? US strerror(errno) : NULL,
+ DEFER, FALSE, &sx->delivery_start);
+ sx->send_quit = FALSE;
+ return DEFER;
+ }
+
+ /* really an error on an SMTP read */
+ message = NULL;
+ sx->send_quit = check_response(sx->conn_args.host, &errno, sx->addrlist->more_errno,
+ sx->buffer, &code, &message, &pass_message);
+ yield = DEFER;
+ goto FAILED;
+
+ SEND_FAILED:
+ code = '4';
+ message = US string_sprintf("smtp send to %s [%s] failed: %s",
+ sx->conn_args.host->name, sx->conn_args.host->address, strerror(errno));
+ sx->send_quit = FALSE;
+ yield = DEFER;
+ goto FAILED;
+
+ EHLOHELO_FAILED:
+ code = '4';
+ message = string_sprintf("Remote host closed connection in response to %s"
+ " (EHLO response was: %s)", smtp_command, sx->buffer);
+ sx->send_quit = FALSE;
+ yield = DEFER;
+ goto FAILED;
+
+ /* This label is jumped to directly when a TLS negotiation has failed,
+ or was not done for a host for which it is required. Values will be set
+ in message and errno, and setting_up will always be true. Treat as
+ a temporary error. */
+
+#ifndef DISABLE_TLS
+ TLS_FAILED:
+ code = '4', yield = DEFER;
+ goto FAILED;
+#endif
+
+ /* The failure happened while setting up the call; see if the failure was
+ a 5xx response (this will either be on connection, or following HELO - a 5xx
+ after EHLO causes it to try HELO). If so, and there are no more hosts to try,
+ fail all addresses, as this host is never going to accept them. For other
+ errors during setting up (timeouts or whatever), defer all addresses, and
+ yield DEFER, so that the host is not tried again for a while.
+
+ XXX This peeking for another host feels like a layering violation. We want
+ to note the host as unusable, but down here we shouldn't know if this was
+ the last host to try for the addr(list). Perhaps the upper layer should be
+ the one to do set_errno() ? The problem is that currently the addr is where
+ errno etc. are stashed, but until we run out of hosts to try the errors are
+ host-specific. Maybe we should enhance the host_item definition? */
+
+FAILED:
+ sx->ok = FALSE; /* For when reached by GOTO */
+ set_errno(sx->addrlist, errno, message,
+ sx->conn_args.host->next
+ ? DEFER
+ : code == '5'
+#ifdef SUPPORT_I18N
+ || errno == ERRNO_UTF8_FWD
+#endif
+ ? FAIL : DEFER,
+ pass_message,
+ errno == ECONNREFUSED ? NULL : sx->conn_args.host,
+#ifdef EXPERIMENTAL_DSN_INFO
+ sx->smtp_greeting, sx->helo_response,
+#endif
+ &sx->delivery_start);
+ }
+
+
+SEND_QUIT:
+
+if (sx->send_quit)
+ (void)smtp_write_command(sx, SCMD_FLUSH, "QUIT\r\n");
+
+#ifndef DISABLE_TLS
+if (sx->cctx.tls_ctx)
+ {
+ if (sx->send_tlsclose)
+ {
+ tls_close(sx->cctx.tls_ctx, TLS_SHUTDOWN_NOWAIT);
+ sx->send_tlsclose = FALSE;
+ }
+ sx->cctx.tls_ctx = NULL;
+ }
+#endif
+
+/* Close the socket, and return the appropriate value, first setting
+works because the NULL setting is passed back to the calling process, and
+remote_max_parallel is forced to 1 when delivering over an existing connection,
+*/
+
+HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" SMTP(close)>>\n");
+if (sx->send_quit)
+ {
+ shutdown(sx->cctx.sock, SHUT_WR);
+ if (fcntl(sx->cctx.sock, F_SETFL, O_NONBLOCK) == 0)
+ for (int i = 16; read(sx->cctx.sock, sx->inbuffer, sizeof(sx->inbuffer)) > 0 && i > 0;)
+ i--; /* drain socket */
+ sx->send_quit = FALSE;
+ }
+(void)close(sx->cctx.sock);
+sx->cctx.sock = -1;
+
+#ifndef DISABLE_EVENT
+(void) event_raise(sx->conn_args.tblock->event_action, US"tcp:close", NULL, NULL);
+#endif
+
+smtp_debug_cmd_report();
+continue_transport = NULL;
+continue_hostname = NULL;
+return yield;
+}
+
+
+
+
+/* Create the string of options that will be appended to the MAIL FROM:
+in the connection context buffer */
+
+static int
+build_mailcmd_options(smtp_context * sx, address_item * addrlist)
+{
+uschar * p = sx->buffer;
+address_item * addr;
+int address_count;
+
+*p = 0;
+
+/* If we know the receiving MTA supports the SIZE qualification, and we know it,
+send it, adding something to the message size to allow for imprecision
+and things that get added en route. Exim keeps the number of lines
+in a message, so we can give an accurate value for the original message, but we
+need some additional to handle added headers. (Double "." characters don't get
+included in the count.) */
+
+if ( message_size > 0
+ && sx->peer_offered & OPTION_SIZE && !(sx->avoid_option & OPTION_SIZE))
+ {
+/*XXX problem here under spool_files_wireformat?
+Or just forget about lines? Or inflate by a fixed proportion? */
+
+ sprintf(CS p, " SIZE=%d", message_size+message_linecount+(SOB sx->conn_args.ob)->size_addition);
+ while (*p) p++;
+ }
+
+#ifndef DISABLE_PRDR
+/* If it supports Per-Recipient Data Responses, and we have more than one recipient,
+request that */
+
+sx->prdr_active = FALSE;
+if (smtp_peer_options & OPTION_PRDR)
+ for (address_item * addr = addrlist; addr; addr = addr->next)
+ if (addr->transport_return == PENDING_DEFER)
+ {
+ for (addr = addr->next; addr; addr = addr->next)
+ if (addr->transport_return == PENDING_DEFER)
+ { /* at least two recipients to send */
+ sx->prdr_active = TRUE;
+ sprintf(CS p, " PRDR"); p += 5;
+ break;
+ }
+ break;
+ }
+#endif
+
+#ifdef SUPPORT_I18N
+/* If it supports internationalised messages, and this meesage need that,
+request it */
+
+if ( sx->peer_offered & OPTION_UTF8
+ && addrlist->prop.utf8_msg
+ && !addrlist->prop.utf8_downcvt
+ )
+ Ustrcpy(p, US" SMTPUTF8"), p += 9;
+#endif
+
+/* check if all addresses have DSN-lasthop flag; do not send RET and ENVID if so */
+for (sx->dsn_all_lasthop = TRUE, addr = addrlist, address_count = 0;
+ addr && address_count < sx->max_rcpt; /*XXX maybe also || sx->single_rcpt_domain ? */
+ addr = addr->next) if (addr->transport_return == PENDING_DEFER)
+ {
+ address_count++;
+ if (!(addr->dsn_flags & rf_dsnlasthop))
+ {
+ sx->dsn_all_lasthop = FALSE;
+ break;
+ }
+ }
+
+/* Add any DSN flags to the mail command */
+
+if (sx->peer_offered & OPTION_DSN && !sx->dsn_all_lasthop)
+ {
+ if (dsn_ret == dsn_ret_hdrs)
+ { Ustrcpy(p, US" RET=HDRS"); p += 9; }
+ else if (dsn_ret == dsn_ret_full)
+ { Ustrcpy(p, US" RET=FULL"); p += 9; }
+
+ if (dsn_envid)
+ {
+ string_format(p, sizeof(sx->buffer) - (p-sx->buffer), " ENVID=%s", dsn_envid);
+ while (*p) p++;
+ }
+ }
+
+/* If an authenticated_sender override has been specified for this transport
+instance, expand it. If the expansion is forced to fail, and there was already
+an authenticated_sender for this message, the original value will be used.
+Other expansion failures are serious. An empty result is ignored, but there is
+otherwise no check - this feature is expected to be used with LMTP and other
+cases where non-standard addresses (e.g. without domains) might be required. */
+
+return smtp_mail_auth_str(sx, p, addrlist) ? ERROR : OK;
+}
+
+
+static void
+build_rcptcmd_options(smtp_context * sx, const address_item * addr)
+{
+uschar * p = sx->buffer;
+*p = 0;
+
+/* Add any DSN flags to the rcpt command */
+
+if (sx->peer_offered & OPTION_DSN && !(addr->dsn_flags & rf_dsnlasthop))
+ {
+ if (addr->dsn_flags & rf_dsnflags)
+ {
+ BOOL first = TRUE;
+
+ Ustrcpy(p, US" NOTIFY=");
+ while (*p) p++;
+ for (int i = 0; i < nelem(rf_list); i++) if (addr->dsn_flags & rf_list[i])
+ {
+ if (!first) *p++ = ',';
+ first = FALSE;
+ Ustrcpy(p, rf_names[i]);
+ while (*p) p++;
+ }
+ }
+
+ if (addr->dsn_orcpt)
+ {
+ string_format(p, sizeof(sx->buffer) - (p-sx->buffer), " ORCPT=%s",
+ addr->dsn_orcpt);
+ while (*p) p++;
+ }
+ }
+}
+
+
+
+/*
+Return:
+ 0 good, rcpt results in addr->transport_return (PENDING_OK, DEFER, FAIL)
+ -1 MAIL response error
+ -2 any non-MAIL read i/o error
+ -3 non-MAIL response timeout
+ -4 internal error; channel still usable
+ -5 transmit failed
+ */
+
+int
+smtp_write_mail_and_rcpt_cmds(smtp_context * sx, int * yield)
+{
+address_item * addr;
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+address_item * restart_addr = NULL;
+#endif
+int address_count, pipe_limit;
+int rc;
+
+if (build_mailcmd_options(sx, sx->first_addr) != OK)
+ {
+ *yield = ERROR;
+ return -4;
+ }
+
+/* From here until we send the DATA command, we can make use of PIPELINING
+if the server host supports it. The code has to be able to check the responses
+at any point, for when the buffer fills up, so we write it totally generally.
+When PIPELINING is off, each command written reports that it has flushed the
+buffer. */
+
+sx->pending_MAIL = TRUE; /* The block starts with MAIL */
+
+ {
+ uschar * s = sx->from_addr;
+#ifdef SUPPORT_I18N
+ uschar * errstr = NULL;
+
+ /* If we must downconvert, do the from-address here. Remember we had to
+ for the to-addresses (done below), and also (ugly) for re-doing when building
+ the delivery log line. */
+
+ if ( sx->addrlist->prop.utf8_msg
+ && (sx->addrlist->prop.utf8_downcvt || !(sx->peer_offered & OPTION_UTF8))
+ )
+ {
+ if (s = string_address_utf8_to_alabel(s, &errstr), errstr)
+ {
+ set_errno_nohost(sx->addrlist, ERRNO_EXPANDFAIL, errstr, DEFER, FALSE, &sx->delivery_start);
+ *yield = ERROR;
+ return -4;
+ }
+ setflag(sx->addrlist, af_utf8_downcvt);
+ }
+#endif
+
+ rc = smtp_write_command(sx, pipelining_active ? SCMD_BUFFER : SCMD_FLUSH,
+ "MAIL FROM:<%s>%s\r\n", s, sx->buffer);
+ }
+
+mail_command = string_copy(big_buffer); /* Save for later error message */
+
+switch(rc)
+ {
+ case -1: /* Transmission error */
+ return -5;
+
+ case +1: /* Cmd was sent */
+ if (!smtp_read_response(sx, sx->buffer, sizeof(sx->buffer), '2',
+ (SOB sx->conn_args.ob)->command_timeout))
+ {
+ if (errno == 0 && sx->buffer[0] == '4')
+ {
+ errno = ERRNO_MAIL4XX;
+ sx->addrlist->more_errno |= ((sx->buffer[1] - '0')*10 + sx->buffer[2] - '0') << 8;
+ }
+ return -1;
+ }
+ sx->pending_MAIL = FALSE;
+ break;
+
+ /* otherwise zero: command queued for pipeline */
+ }
+
+/* Pass over all the relevant recipient addresses for this host, which are the
+ones that have status PENDING_DEFER. If we are using PIPELINING, we can send
+several before we have to read the responses for those seen so far. This
+checking is done by a subroutine because it also needs to be done at the end.
+Send only up to max_rcpt addresses at a time, leaving next_addr pointing to
+the next one if not all are sent.
+
+In the MUA wrapper situation, we want to flush the PIPELINING buffer for the
+last address because we want to abort if any recipients have any kind of
+problem, temporary or permanent. We know that all recipient addresses will have
+the PENDING_DEFER status, because only one attempt is ever made, and we know
+that max_rcpt will be large, so all addresses will be done at once.
+
+For verify we flush the pipeline after any (the only) rcpt address. */
+
+for (addr = sx->first_addr, address_count = 0, pipe_limit = 100;
+ addr && address_count < sx->max_rcpt;
+ addr = addr->next) if (addr->transport_return == PENDING_DEFER)
+ {
+ int cmds_sent;
+ BOOL no_flush;
+ uschar * rcpt_addr;
+
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+ if ( sx->single_rcpt_domain /* restriction on domains */
+ && address_count > 0 /* not first being sent */
+ && Ustrcmp(addr->domain, sx->first_addr->domain) != 0 /* dom diff from first */
+ )
+ {
+ DEBUG(D_transport) debug_printf("skipping different domain %s\n", addr->domain);
+
+ /* Ensure the smtp-response reaper does not think the address had a RCPT
+ command sent for it. Reset to PENDING_DEFER in smtp_deliver(), where we
+ goto SEND_MESSAGE. */
+
+ addr->transport_return = SKIP;
+ if (!restart_addr) restart_addr = addr; /* note restart point */
+ continue; /* skip this one */
+ }
+#endif
+
+ addr->dsn_aware = sx->peer_offered & OPTION_DSN
+ ? dsn_support_yes : dsn_support_no;
+
+ address_count++;
+ if (pipe_limit-- <= 0)
+ { no_flush = FALSE; pipe_limit = 100; }
+ else
+ no_flush = pipelining_active && !sx->verify
+ && (!mua_wrapper || addr->next && address_count < sx->max_rcpt);
+
+ build_rcptcmd_options(sx, addr);
+
+ /* Now send the RCPT command, and process outstanding responses when
+ necessary. After a timeout on RCPT, we just end the function, leaving the
+ yield as OK, because this error can often mean that there is a problem with
+ just one address, so we don't want to delay the host. */
+
+ rcpt_addr = transport_rcpt_address(addr, sx->conn_args.tblock->rcpt_include_affixes);
+
+#ifdef SUPPORT_I18N
+ if ( testflag(sx->addrlist, af_utf8_downcvt)
+ && !(rcpt_addr = string_address_utf8_to_alabel(rcpt_addr, NULL))
+ )
+ {
+ /*XXX could we use a per-address errstr here? Not fail the whole send? */
+ errno = ERRNO_EXPANDFAIL;
+ return -5; /*XXX too harsh? */
+ }
+#endif
+
+ cmds_sent = smtp_write_command(sx, no_flush ? SCMD_BUFFER : SCMD_FLUSH,
+ "RCPT TO:<%s>%s%s\r\n", rcpt_addr, sx->igquotstr, sx->buffer);
+
+ if (cmds_sent < 0) return -5;
+ if (cmds_sent > 0)
+ {
+ switch(sync_responses(sx, cmds_sent, 0))
+ {
+ case 3: sx->ok = TRUE; /* 2xx & 5xx => OK & progress made */
+ case 2: sx->completed_addr = TRUE; /* 5xx (only) => progress made */
+ break;
+
+ case 1: sx->ok = TRUE; /* 2xx (only) => OK, but if LMTP, */
+ if (!sx->lmtp) /* can't tell about progress yet */
+ sx->completed_addr = TRUE;
+ case 0: /* No 2xx or 5xx, but no probs */
+ /* If any RCPT got a 452 response then next_addr has been updated
+ for restarting with a new MAIL on the same connection. Send no more
+ RCPTs for this MAIL. */
+
+ if (sx->RCPT_452)
+ {
+ DEBUG(D_transport) debug_printf("seen 452 too-many-rcpts\n");
+ sx->RCPT_452 = FALSE;
+ /* sx->next_addr has been reset for fast_retry */
+ return 0;
+ }
+ break;
+
+ case -1: return -3; /* Timeout on RCPT */
+ case -2: return -2; /* non-MAIL read i/o error */
+ default: return -1; /* any MAIL error */
+
+#ifndef DISABLE_PIPE_CONNECT
+ case -4: return -1; /* non-2xx for pipelined banner or EHLO */
+ case -5: return -1; /* TLS first-read error */
+#endif
+ }
+ }
+ } /* Loop for next address */
+
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+sx->next_addr = restart_addr ? restart_addr : addr;
+#else
+sx->next_addr = addr;
+#endif
+return 0;
+}
+
+
+#ifndef DISABLE_TLS
+/*****************************************************
+* Proxy TLS connection for another transport process *
+******************************************************/
+/*
+Close the unused end of the pipe, fork once more, then use the given buffer
+as a staging area, and select on both the given fd and the TLS'd client-fd for
+data to read (per the coding in ip_recv() and fd_ready() this is legitimate).
+Do blocking full-size writes, and reads under a timeout. Once both input
+channels are closed, exit the process.
+
+Arguments:
+ ct_ctx tls context
+ buf space to use for buffering
+ bufsiz size of buffer
+ pfd pipe filedescriptor array; [0] is comms to proxied process
+ timeout per-read timeout, seconds
+ host hostname of remote
+
+Does not return.
+*/
+
+void
+smtp_proxy_tls(void * ct_ctx, uschar * buf, size_t bsize, int * pfd,
+ int timeout, const uschar * host)
+{
+struct pollfd p[2] = {{.fd = tls_out.active.sock, .events = POLLIN},
+ {.fd = pfd[0], .events = POLLIN}};
+int rc, i;
+BOOL send_tls_shutdown = TRUE;
+
+close(pfd[1]);
+if ((rc = exim_fork(US"tls-proxy")))
+ _exit(rc < 0 ? EXIT_FAILURE : EXIT_SUCCESS);
+
+set_process_info("proxying TLS connection for continued transport to %s\n", host);
+
+do
+ {
+ time_t time_left = timeout;
+ time_t time_start = time(NULL);
+
+ /* wait for data */
+ do
+ {
+ rc = poll(p, 2, time_left * 1000);
+
+ if (rc < 0 && errno == EINTR)
+ if ((time_left -= time(NULL) - time_start) > 0) continue;
+
+ if (rc <= 0)
+ {
+ DEBUG(D_transport) if (rc == 0) debug_printf("%s: timed out\n", __FUNCTION__);
+ goto done;
+ }
+
+ /* For errors where not readable, bomb out */
+
+ if (p[0].revents & POLLERR || p[1].revents & POLLERR)
+ {
+ DEBUG(D_transport) debug_printf("select: exceptional cond on %s fd\n",
+ p[0].revents & POLLERR ? "tls" : "proxy");
+ if (!(p[0].revents & POLLIN || p[1].events & POLLIN))
+ goto done;
+ DEBUG(D_transport) debug_printf("- but also readable; no exit yet\n");
+ }
+ }
+ while (rc < 0 || !(p[0].revents & POLLIN || p[1].revents & POLLIN));
+
+ /* handle inbound data */
+ if (p[0].revents & POLLIN)
+ if ((rc = tls_read(ct_ctx, buf, bsize)) <= 0) /* Expect -1 for EOF; */
+ { /* that reaps the TLS Close Notify record */
+ p[0].fd = -1;
+ shutdown(pfd[0], SHUT_WR);
+ timeout = 5;
+ }
+ else
+ for (int nbytes = 0; rc - nbytes > 0; nbytes += i)
+ if ((i = write(pfd[0], buf + nbytes, rc - nbytes)) < 0) goto done;
+
+ /* Handle outbound data. We cannot combine payload and the TLS-close
+ due to the limitations of the (pipe) channel feeding us. Maybe use a unix-domain
+ socket? */
+ if (p[1].revents & POLLIN)
+ if ((rc = read(pfd[0], buf, bsize)) <= 0)
+ {
+ p[1].fd = -1;
+
+# ifdef EXIM_TCP_CORK /* Use _CORK to get TLS Close Notify in FIN segment */
+ (void) setsockopt(tls_out.active.sock, IPPROTO_TCP, EXIM_TCP_CORK, US &on, sizeof(on));
+# endif
+ tls_shutdown_wr(ct_ctx);
+ send_tls_shutdown = FALSE;
+ shutdown(tls_out.active.sock, SHUT_WR);
+ }
+ else
+ for (int nbytes = 0; rc - nbytes > 0; nbytes += i)
+ if ((i = tls_write(ct_ctx, buf + nbytes, rc - nbytes, FALSE)) < 0)
+ goto done;
+ }
+while (p[0].fd >= 0 || p[1].fd >= 0);
+
+done:
+ if (send_tls_shutdown) tls_close(ct_ctx, TLS_SHUTDOWN_NOWAIT);
+ ct_ctx = NULL;
+ testharness_pause_ms(100); /* let logging complete */
+ exim_exit(EXIT_SUCCESS);
+}
+#endif
+
+
+/*************************************************
+* Deliver address list to given host *
+*************************************************/
+
+/* If continue_hostname is not null, we get here only when continuing to
+deliver down an existing channel. The channel was passed as the standard
+input. TLS is never active on a passed channel; the previous process either
+closes it down before passing the connection on, or inserts a TLS-proxy
+process and passes on a cleartext conection.
+
+Otherwise, we have to make a connection to the remote host, and do the
+initial protocol exchange.
+
+When running as an MUA wrapper, if the sender or any recipient is rejected,
+temporarily or permanently, we force failure for all recipients.
+
+Arguments:
+ addrlist chain of potential addresses to deliver; only those whose
+ transport_return field is set to PENDING_DEFER are currently
+ being processed; others should be skipped - they have either
+ been delivered to an earlier host or IP address, or been
+ failed by one of them.
+ host host to deliver to
+ host_af AF_INET or AF_INET6
+ defport default TCP/IP port to use if host does not specify, in host
+ byte order
+ interface interface to bind to, or NULL
+ tblock transport instance block
+ message_defer set TRUE if yield is OK, but all addresses were deferred
+ because of a non-recipient, non-host failure, that is, a
+ 4xx response to MAIL FROM, DATA, or ".". This is a defer
+ that is specific to the message.
+ suppress_tls if TRUE, don't attempt a TLS connection - this is set for
+ a second attempt after TLS initialization fails
+
+Returns: OK - the connection was made and the delivery attempted;
+ the result for each address is in its data block.
+ DEFER - the connection could not be made, or something failed
+ while setting up the SMTP session, or there was a
+ non-message-specific error, such as a timeout.
+ ERROR - a filter command is specified for this transport,
+ and there was a problem setting it up; OR helo_data
+ or add_headers or authenticated_sender is specified
+ for this transport, and the string failed to expand
+
+ For all non-OK returns the first addr of the list carries the
+ time taken for the attempt.
+*/
+
+static int
+smtp_deliver(address_item *addrlist, host_item *host, int host_af, int defport,
+ uschar *interface, transport_instance *tblock,
+ BOOL *message_defer, BOOL suppress_tls)
+{
+smtp_transport_options_block * ob = SOB tblock->options_block;
+int yield = OK;
+int save_errno;
+int rc;
+
+uschar *message = NULL;
+uschar new_message_id[MESSAGE_ID_LENGTH + 1];
+smtp_context * sx = store_get(sizeof(*sx), GET_TAINTED); /* tainted, for the data buffers */
+BOOL pass_message = FALSE;
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+BOOL mail_limit = FALSE;
+#endif
+#ifdef SUPPORT_DANE
+BOOL dane_held;
+#endif
+BOOL tcw_done = FALSE, tcw = FALSE;
+
+*message_defer = FALSE;
+
+memset(sx, 0, sizeof(*sx));
+sx->addrlist = addrlist;
+sx->conn_args.host = host;
+sx->conn_args.host_af = host_af;
+sx->port = defport;
+sx->conn_args.interface = interface;
+sx->helo_data = NULL;
+sx->conn_args.tblock = tblock;
+sx->conn_args.sock = -1;
+gettimeofday(&sx->delivery_start, NULL);
+sx->sync_addr = sx->first_addr = addrlist;
+
+REPEAT_CONN:
+#ifdef SUPPORT_DANE
+dane_held = FALSE;
+#endif
+
+/* Get the channel set up ready for a message, MAIL FROM being the next
+SMTP command to send. */
+
+if ((rc = smtp_setup_conn(sx, suppress_tls)) != OK)
+ {
+ timesince(&addrlist->delivery_time, &sx->delivery_start);
+ yield = rc;
+ goto TIDYUP;
+ }
+
+#ifdef SUPPORT_DANE
+/* If the connection used DANE, ignore for now any addresses with incompatible
+domains. The SNI has to be the domain. Arrange a whole new TCP conn later,
+just in case only TLS isn't enough. */
+
+if (sx->conn_args.dane)
+ {
+ const uschar * dane_domain = sx->first_addr->domain;
+
+ for (address_item * a = sx->first_addr->next; a; a = a->next)
+ if ( a->transport_return == PENDING_DEFER
+ && Ustrcmp(dane_domain, a->domain) != 0)
+ {
+ DEBUG(D_transport) debug_printf("DANE: holding %s for later\n", a->domain);
+ dane_held = TRUE;
+ a->transport_return = DANE;
+ }
+ }
+#endif
+
+/* If there is a filter command specified for this transport, we can now
+set it up. This cannot be done until the identity of the host is known. */
+
+if (tblock->filter_command)
+ {
+ transport_filter_timeout = tblock->filter_timeout;
+
+ /* On failure, copy the error to all addresses, abandon the SMTP call, and
+ yield ERROR. */
+
+ if (!transport_set_up_command(&transport_filter_argv,
+ tblock->filter_command, TRUE, DEFER, addrlist, FALSE,
+ string_sprintf("%.50s transport filter", tblock->name), NULL))
+ {
+ set_errno_nohost(addrlist->next, addrlist->basic_errno, addrlist->message, DEFER,
+ FALSE, &sx->delivery_start);
+ yield = ERROR;
+ goto SEND_QUIT;
+ }
+
+ if ( transport_filter_argv
+ && *transport_filter_argv
+ && **transport_filter_argv
+ && smtp_peer_options & OPTION_CHUNKING
+#ifndef DISABLE_DKIM
+ /* When dkim signing, chunking is handled even with a transport-filter */
+ && !(ob->dkim.dkim_private_key && ob->dkim.dkim_domain && ob->dkim.dkim_selector)
+ && !ob->dkim.force_bodyhash
+#endif
+ )
+ {
+ smtp_peer_options &= ~OPTION_CHUNKING;
+ DEBUG(D_transport) debug_printf("CHUNKING not usable due to transport filter\n");
+ }
+ }
+
+/* For messages that have more than the maximum number of envelope recipients,
+we want to send several transactions down the same SMTP connection. (See
+comments in deliver.c as to how this reconciles, heuristically, with
+remote_max_parallel.) This optimization was added to Exim after the following
+code was already working. The simplest way to put it in without disturbing the
+code was to use a goto to jump back to this point when there is another
+transaction to handle. */
+
+SEND_MESSAGE:
+sx->from_addr = return_path;
+sx->sync_addr = sx->first_addr;
+sx->ok = FALSE;
+sx->send_rset = TRUE;
+sx->completed_addr = FALSE;
+
+
+/* If we are a continued-connection-after-verify the MAIL and RCPT
+commands were already sent; do not re-send but do mark the addrs as
+having been accepted up to RCPT stage. A traditional cont-conn
+always has a sequence number greater than one. */
+
+if (continue_hostname && continue_sequence == 1)
+ {
+ /* sx->pending_MAIL = FALSE; */
+ sx->ok = TRUE;
+ /* sx->next_addr = NULL; */
+
+ for (address_item * addr = addrlist; addr; addr = addr->next)
+ addr->transport_return = PENDING_OK;
+ }
+else
+ {
+ /* Initiate a message transfer. */
+
+ switch(smtp_write_mail_and_rcpt_cmds(sx, &yield))
+ {
+ case 0: break;
+ case -1: case -2: goto RESPONSE_FAILED;
+ case -3: goto END_OFF;
+ case -4: goto SEND_QUIT;
+ default: goto SEND_FAILED;
+ }
+
+ /* If we are an MUA wrapper, abort if any RCPTs were rejected, either
+ permanently or temporarily. We should have flushed and synced after the last
+ RCPT. */
+
+ if (mua_wrapper)
+ {
+ address_item * a;
+ unsigned cnt;
+
+ for (a = sx->first_addr, cnt = 0; a && cnt < sx->max_rcpt; a = a->next, cnt++)
+ if (a->transport_return != PENDING_OK)
+ {
+ /*XXX could we find a better errno than 0 here? */
+ set_errno_nohost(addrlist, 0, a->message, FAIL,
+ testflag(a, af_pass_message), &sx->delivery_start);
+ sx->ok = FALSE;
+ break;
+ }
+ }
+ }
+
+/* If ok is TRUE, we know we have got at least one good recipient, and must now
+send DATA, but if it is FALSE (in the normal, non-wrapper case), we may still
+have a good recipient buffered up if we are pipelining. We don't want to waste
+time sending DATA needlessly, so we only send it if either ok is TRUE or if we
+are pipelining. The responses are all handled by sync_responses().
+If using CHUNKING, do not send a BDAT until we know how big a chunk we want
+to send is. */
+
+if ( !(smtp_peer_options & OPTION_CHUNKING)
+ && (sx->ok || (pipelining_active && !mua_wrapper)))
+ {
+ int count = smtp_write_command(sx, SCMD_FLUSH, "DATA\r\n");
+
+ if (count < 0) goto SEND_FAILED;
+ switch(sync_responses(sx, count, sx->ok ? +1 : -1))
+ {
+ case 3: sx->ok = TRUE; /* 2xx & 5xx => OK & progress made */
+ case 2: sx->completed_addr = TRUE; /* 5xx (only) => progress made */
+ break;
+
+ case 1: sx->ok = TRUE; /* 2xx (only) => OK, but if LMTP, */
+ if (!sx->lmtp) sx->completed_addr = TRUE; /* can't tell about progress yet */
+ case 0: break; /* No 2xx or 5xx, but no probs */
+
+ case -1: goto END_OFF; /* Timeout on RCPT */
+
+#ifndef DISABLE_PIPE_CONNECT
+ case -5: /* TLS first-read error */
+ case -4: HDEBUG(D_transport)
+ debug_printf("failed reaping pipelined cmd responses\n");
+#endif
+ default: goto RESPONSE_FAILED; /* I/O error, or any MAIL/DATA error */
+ }
+ pipelining_active = FALSE;
+ data_command = string_copy(big_buffer); /* Save for later error message */
+ }
+
+/* If there were no good recipients (but otherwise there have been no
+problems), just set ok TRUE, since we have handled address-specific errors
+already. Otherwise, it's OK to send the message. Use the check/escape mechanism
+for handling the SMTP dot-handling protocol, flagging to apply to headers as
+well as body. Set the appropriate timeout value to be used for each chunk.
+(Haven't been able to make it work using select() for writing yet.) */
+
+if ( !sx->ok
+ && (!(smtp_peer_options & OPTION_CHUNKING) || !pipelining_active))
+ {
+ /* Save the first address of the next batch. */
+ sx->first_addr = sx->next_addr;
+
+ sx->ok = TRUE;
+ }
+else
+ {
+ transport_ctx tctx = {
+ .u = {.fd = sx->cctx.sock}, /*XXX will this need TLS info? */
+ .tblock = tblock,
+ .addr = addrlist,
+ .check_string = US".",
+ .escape_string = US"..", /* Escaping strings */
+ .options =
+ topt_use_crlf | topt_escape_headers
+ | (tblock->body_only ? topt_no_headers : 0)
+ | (tblock->headers_only ? topt_no_body : 0)
+ | (tblock->return_path_add ? topt_add_return_path : 0)
+ | (tblock->delivery_date_add ? topt_add_delivery_date : 0)
+ | (tblock->envelope_to_add ? topt_add_envelope_to : 0)
+ };
+
+ /* If using CHUNKING we need a callback from the generic transport
+ support to us, for the sending of BDAT smtp commands and the reaping
+ of responses. The callback needs a whole bunch of state so set up
+ a transport-context structure to be passed around. */
+
+ if (smtp_peer_options & OPTION_CHUNKING)
+ {
+ tctx.check_string = tctx.escape_string = NULL;
+ tctx.options |= topt_use_bdat;
+ tctx.chunk_cb = smtp_chunk_cmd_callback;
+ sx->pending_BDAT = FALSE;
+ sx->good_RCPT = sx->ok;
+ sx->cmd_count = 0;
+ tctx.smtp_context = sx;
+ }
+ else
+ tctx.options |= topt_end_dot;
+
+ /* Save the first address of the next batch. */
+ sx->first_addr = sx->next_addr;
+
+ /* Responses from CHUNKING commands go in buffer. Otherwise,
+ there has not been a response. */
+
+ sx->buffer[0] = 0;
+
+ sigalrm_seen = FALSE;
+ transport_write_timeout = ob->data_timeout;
+ smtp_command = US"sending data block"; /* For error messages */
+ DEBUG(D_transport|D_v)
+ if (smtp_peer_options & OPTION_CHUNKING)
+ debug_printf(" will write message using CHUNKING\n");
+ else
+ debug_printf(" SMTP>> (writing message)\n");
+ transport_count = 0;
+
+#ifndef DISABLE_DKIM
+ {
+# ifdef MEASURE_TIMING
+ struct timeval t0;
+ gettimeofday(&t0, NULL);
+# endif
+ dkim_exim_sign_init();
+# ifdef EXPERIMENTAL_ARC
+ {
+ uschar * s = ob->arc_sign;
+ if (s)
+ {
+ if (!(ob->dkim.arc_signspec = s = expand_string(s)))
+ {
+ if (!f.expand_string_forcedfail)
+ {
+ message = US"failed to expand arc_sign";
+ sx->ok = FALSE;
+ goto SEND_FAILED;
+ }
+ }
+ else if (*s)
+ {
+ /* Ask dkim code to hash the body for ARC */
+ (void) arc_ams_setup_sign_bodyhash();
+ ob->dkim.force_bodyhash = TRUE;
+ }
+ }
+ }
+# endif
+# ifdef MEASURE_TIMING
+ report_time_since(&t0, US"dkim_exim_sign_init (delta)");
+# endif
+ }
+#endif
+
+ /* See if we can pipeline QUIT. Reasons not to are
+ - pipelining not active
+ - not ok to send quit
+ - errors in amtp transation responses
+ - more addrs to send for this message or this host
+ - this message was being retried
+ - more messages for this host
+ If we can, we want the message-write to not flush (the tail end of) its data out. */
+
+ if ( sx->pipelining_used
+ && (sx->ok && sx->completed_addr || smtp_peer_options & OPTION_CHUNKING)
+ && sx->send_quit
+ && !(sx->first_addr || f.continue_more)
+ && f.deliver_firsttime
+ )
+ {
+ smtp_compare_t t_compare =
+ {.tblock = tblock, .current_sender_address = sender_address};
+
+ tcw_done = TRUE;
+ tcw =
+#ifndef DISABLE_TLS
+ ( tls_out.active.sock < 0 && !continue_proxy_cipher
+ || verify_check_given_host(CUSS &ob->hosts_nopass_tls, host) != OK
+ )
+ &&
+#endif
+ transport_check_waiting(tblock->name, host->name,
+ tblock->connection_max_messages, new_message_id,
+ (oicf)smtp_are_same_identities, (void*)&t_compare);
+ if (!tcw)
+ {
+ HDEBUG(D_transport) debug_printf("will pipeline QUIT\n");
+ tctx.options |= topt_no_flush;
+ }
+ }
+
+#ifndef DISABLE_DKIM
+ sx->ok = dkim_transport_write_message(&tctx, &ob->dkim, CUSS &message);
+#else
+ sx->ok = transport_write_message(&tctx, 0);
+#endif
+
+ /* transport_write_message() uses write() because it is called from other
+ places to write to non-sockets. This means that under some OS (e.g. Solaris)
+ it can exit with "Broken pipe" as its error. This really means that the
+ socket got closed at the far end. */
+
+ transport_write_timeout = 0; /* for subsequent transports */
+
+ /* Failure can either be some kind of I/O disaster (including timeout),
+ or the failure of a transport filter or the expansion of added headers.
+ Or, when CHUNKING, it can be a protocol-detected failure. */
+
+ if (!sx->ok)
+ if (message) goto SEND_FAILED;
+ else goto RESPONSE_FAILED;
+
+ /* We used to send the terminating "." explicitly here, but because of
+ buffering effects at both ends of TCP/IP connections, you don't gain
+ anything by keeping it separate, so it might as well go in the final
+ data buffer for efficiency. This is now done by setting the topt_end_dot
+ flag above. */
+
+ smtp_command = US"end of data";
+
+ /* If we can pipeline a QUIT with the data them send it now. If a new message
+ for this host appeared in the queue while data was being sent, we will not see
+ it and it will have to wait for a queue run. If there was one but another
+ thread took it, we might attempt to send it - but locking of spoolfiles will
+ detect that. Use _MORE to get QUIT in FIN segment. */
+
+ if (tcw_done && !tcw)
+ {
+ /*XXX jgh 2021/03/10 google et. al screwup. G, at least, sends TCP FIN in response to TLS
+ close-notify. Under TLS 1.3, violating RFC.
+ However, TLS 1.2 does not have half-close semantics. */
+
+ if ( sx->cctx.tls_ctx
+#if 0 && !defined(DISABLE_TLS)
+ && Ustrcmp(tls_out.ver, "TLS1.3") != 0
+#endif
+ || !f.deliver_firsttime
+ )
+ { /* Send QUIT now and not later */
+ (void)smtp_write_command(sx, SCMD_FLUSH, "QUIT\r\n");
+ sx->send_quit = FALSE;
+ }
+ else
+ { /* add QUIT to the output buffer */
+ (void)smtp_write_command(sx, SCMD_MORE, "QUIT\r\n");
+ sx->send_quit = FALSE; /* avoid sending it later */
+
+#ifndef DISABLE_TLS
+ if (sx->cctx.tls_ctx && sx->send_tlsclose) /* need to send TLS Close Notify */
+ {
+# ifdef EXIM_TCP_CORK /* Use _CORK to get Close Notify in FIN segment */
+ (void) setsockopt(sx->cctx.sock, IPPROTO_TCP, EXIM_TCP_CORK, US &on, sizeof(on));
+# endif
+ tls_shutdown_wr(sx->cctx.tls_ctx);
+ sx->send_tlsclose = FALSE; /* avoid later repeat */
+ }
+#endif
+ HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" SMTP(shutdown)>>\n");
+ shutdown(sx->cctx.sock, SHUT_WR); /* flush output buffer, with TCP FIN */
+ }
+ }
+
+ if (smtp_peer_options & OPTION_CHUNKING && sx->cmd_count > 1)
+ {
+ /* Reap any outstanding MAIL & RCPT commands, but not a DATA-go-ahead */
+ switch(sync_responses(sx, sx->cmd_count-1, 0))
+ {
+ case 3: sx->ok = TRUE; /* 2xx & 5xx => OK & progress made */
+ case 2: sx->completed_addr = TRUE; /* 5xx (only) => progress made */
+ break;
+
+ case 1: sx->ok = TRUE; /* 2xx (only) => OK, but if LMTP, */
+ if (!sx->lmtp) sx->completed_addr = TRUE; /* can't tell about progress yet */
+ case 0: break; /* No 2xx or 5xx, but no probs */
+
+ case -1: goto END_OFF; /* Timeout on RCPT */
+
+#ifndef DISABLE_PIPE_CONNECT
+ case -5: /* TLS first-read error */
+ case -4: HDEBUG(D_transport)
+ debug_printf("failed reaping pipelined cmd responses\n");
+#endif
+ default: goto RESPONSE_FAILED; /* I/O error, or any MAIL/DATA error */
+ }
+ }
+
+#ifndef DISABLE_PRDR
+ /* For PRDR we optionally get a partial-responses warning followed by the
+ individual responses, before going on with the overall response. If we don't
+ get the warning then deal with per non-PRDR. */
+
+ if(sx->prdr_active)
+ {
+ sx->ok = smtp_read_response(sx, sx->buffer, sizeof(sx->buffer), '3', ob->final_timeout);
+ if (!sx->ok && errno == 0) switch(sx->buffer[0])
+ {
+ case '2': sx->prdr_active = FALSE;
+ sx->ok = TRUE;
+ break;
+ case '4': errno = ERRNO_DATA4XX;
+ addrlist->more_errno |=
+ ((sx->buffer[1] - '0')*10 + sx->buffer[2] - '0') << 8;
+ break;
+ }
+ }
+ else
+#endif
+
+ /* For non-PRDR SMTP, we now read a single response that applies to the
+ whole message. If it is OK, then all the addresses have been delivered. */
+
+ if (!sx->lmtp)
+ {
+ sx->ok = smtp_read_response(sx, sx->buffer, sizeof(sx->buffer), '2',
+ ob->final_timeout);
+ if (!sx->ok && errno == 0 && sx->buffer[0] == '4')
+ {
+ errno = ERRNO_DATA4XX;
+ addrlist->more_errno |= ((sx->buffer[1] - '0')*10 + sx->buffer[2] - '0') << 8;
+ }
+ }
+
+ /* For LMTP, we get back a response for every RCPT command that we sent;
+ some may be accepted and some rejected. For those that get a response, their
+ status is fixed; any that are accepted have been handed over, even if later
+ responses crash - at least, that's how I read RFC 2033.
+
+ If all went well, mark the recipient addresses as completed, record which
+ host/IPaddress they were delivered to, and cut out RSET when sending another
+ message down the same channel. Write the completed addresses to the journal
+ now so that they are recorded in case there is a crash of hardware or
+ software before the spool gets updated. Also record the final SMTP
+ confirmation if needed (for SMTP only). */
+
+ if (sx->ok)
+ {
+ int flag = '=';
+ struct timeval delivery_time;
+ int len;
+ uschar * conf = NULL;
+
+ timesince(&delivery_time, &sx->delivery_start);
+ sx->send_rset = FALSE;
+ pipelining_active = FALSE;
+
+ /* Set up confirmation if needed - applies only to SMTP */
+
+ if (
+#ifdef DISABLE_EVENT
+ LOGGING(smtp_confirmation) &&
+#endif
+ !sx->lmtp
+ )
+ {
+ const uschar * s = string_printing(sx->buffer);
+ /* deconst cast ok here as string_printing was checked to have alloc'n'copied */
+ conf = s == sx->buffer ? US string_copy(s) : US s;
+ }
+
+ /* Process all transported addresses - for LMTP or PRDR, read a status for
+ each one. We used to drop out at first_addr, until someone returned a 452
+ followed by a 250... and we screwed up the accepted addresses. */
+
+ for (address_item * addr = addrlist; addr; addr = addr->next)
+ {
+ if (addr->transport_return != PENDING_OK) continue;
+
+ /* LMTP - if the response fails badly (e.g. timeout), use it for all the
+ remaining addresses. Otherwise, it's a return code for just the one
+ address. For temporary errors, add a retry item for the address so that
+ it doesn't get tried again too soon. */
+
+#ifndef DISABLE_PRDR
+ if (sx->lmtp || sx->prdr_active)
+#else
+ if (sx->lmtp)
+#endif
+ {
+ if (!smtp_read_response(sx, sx->buffer, sizeof(sx->buffer), '2',
+ ob->final_timeout))
+ {
+ if (errno != 0 || sx->buffer[0] == 0) goto RESPONSE_FAILED;
+ addr->message = string_sprintf(
+#ifndef DISABLE_PRDR
+ "%s error after %s: %s", sx->prdr_active ? "PRDR":"LMTP",
+#else
+ "LMTP error after %s: %s",
+#endif
+ data_command, string_printing(sx->buffer));
+ setflag(addr, af_pass_message); /* Allow message to go to user */
+ if (sx->buffer[0] == '5')
+ addr->transport_return = FAIL;
+ else
+ {
+ errno = ERRNO_DATA4XX;
+ addr->more_errno |= ((sx->buffer[1] - '0')*10 + sx->buffer[2] - '0') << 8;
+ addr->transport_return = DEFER;
+#ifndef DISABLE_PRDR
+ if (!sx->prdr_active)
+#endif
+ retry_add_item(addr, addr->address_retry_key, 0);
+ }
+ continue;
+ }
+ sx->completed_addr = TRUE; /* NOW we can set this flag */
+ if (LOGGING(smtp_confirmation))
+ {
+ const uschar *s = string_printing(sx->buffer);
+ /* deconst cast ok here as string_printing was checked to have alloc'n'copied */
+ conf = (s == sx->buffer) ? US string_copy(s) : US s;
+ }
+ }
+
+ /* SMTP, or success return from LMTP for this address. Pass back the
+ actual host that was used. */
+
+ addr->transport_return = OK;
+ addr->host_used = host;
+ addr->delivery_time = delivery_time;
+ addr->special_action = flag;
+ addr->message = conf;
+
+ if (tcp_out_fastopen)
+ {
+ setflag(addr, af_tcp_fastopen_conn);
+ if (tcp_out_fastopen >= TFO_USED_NODATA) setflag(addr, af_tcp_fastopen);
+ if (tcp_out_fastopen >= TFO_USED_DATA) setflag(addr, af_tcp_fastopen_data);
+ }
+ if (sx->pipelining_used) setflag(addr, af_pipelining);
+#ifndef DISABLE_PIPE_CONNECT
+ if (sx->early_pipe_active) setflag(addr, af_early_pipe);
+#endif
+#ifndef DISABLE_PRDR
+ if (sx->prdr_active) setflag(addr, af_prdr_used);
+#endif
+ if (smtp_peer_options & OPTION_CHUNKING) setflag(addr, af_chunking_used);
+ flag = '-';
+
+#ifndef DISABLE_PRDR
+ if (!sx->prdr_active)
+#endif
+ {
+ /* Update the journal. For homonymic addresses, use the base address plus
+ the transport name. See lots of comments in deliver.c about the reasons
+ for the complications when homonyms are involved. Just carry on after
+ write error, as it may prove possible to update the spool file later. */
+
+ if (testflag(addr, af_homonym))
+ sprintf(CS sx->buffer, "%.500s/%s\n", addr->unique + 3, tblock->name);
+ else
+ sprintf(CS sx->buffer, "%.500s\n", addr->unique);
+
+ DEBUG(D_deliver) debug_printf("S:journalling %s", sx->buffer);
+ len = Ustrlen(CS sx->buffer);
+ if (write(journal_fd, sx->buffer, len) != len)
+ log_write(0, LOG_MAIN|LOG_PANIC, "failed to write journal for "
+ "%s: %s", sx->buffer, strerror(errno));
+ }
+ }
+
+#ifndef DISABLE_PRDR
+ if (sx->prdr_active)
+ {
+ const uschar * overall_message;
+
+ /* PRDR - get the final, overall response. For any non-success
+ upgrade all the address statuses. */
+
+ sx->ok = smtp_read_response(sx, sx->buffer, sizeof(sx->buffer), '2',
+ ob->final_timeout);
+ if (!sx->ok)
+ {
+ if(errno == 0 && sx->buffer[0] == '4')
+ {
+ errno = ERRNO_DATA4XX;
+ addrlist->more_errno |= ((sx->buffer[1] - '0')*10 + sx->buffer[2] - '0') << 8;
+ }
+ for (address_item * addr = addrlist; addr != sx->first_addr; addr = addr->next)
+ if (sx->buffer[0] == '5' || addr->transport_return == OK)
+ addr->transport_return = PENDING_OK; /* allow set_errno action */
+ goto RESPONSE_FAILED;
+ }
+
+ /* Append the overall response to the individual PRDR response for logging
+ and update the journal, or setup retry. */
+
+ overall_message = string_printing(sx->buffer);
+ for (address_item * addr = addrlist; addr != sx->first_addr; addr = addr->next)
+ if (addr->transport_return == OK)
+ addr->message = string_sprintf("%s\\n%s", addr->message, overall_message);
+
+ for (address_item * addr = addrlist; addr != sx->first_addr; addr = addr->next)
+ if (addr->transport_return == OK)
+ {
+ if (testflag(addr, af_homonym))
+ sprintf(CS sx->buffer, "%.500s/%s\n", addr->unique + 3, tblock->name);
+ else
+ sprintf(CS sx->buffer, "%.500s\n", addr->unique);
+
+ DEBUG(D_deliver) debug_printf("journalling(PRDR) %s\n", sx->buffer);
+ len = Ustrlen(CS sx->buffer);
+ if (write(journal_fd, sx->buffer, len) != len)
+ log_write(0, LOG_MAIN|LOG_PANIC, "failed to write journal for "
+ "%s: %s", sx->buffer, strerror(errno));
+ }
+ else if (addr->transport_return == DEFER)
+ /*XXX magic value -2 ? maybe host+message ? */
+ retry_add_item(addr, addr->address_retry_key, -2);
+ }
+#endif
+
+ /* 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));
+ }
+ }
+
+
+/* Handle general (not specific to one address) failures here. The value of ok
+is used to skip over this code on the falling through case. A timeout causes a
+deferral. Other errors may defer or fail according to the response code, and
+may set up a special errno value, e.g. after connection chopped, which is
+assumed if errno == 0 and there is no text in the buffer. If control reaches
+here during the setting up phase (i.e. before MAIL FROM) then always defer, as
+the problem is not related to this specific message. */
+
+if (!sx->ok)
+ {
+ int code, set_rc;
+ uschar * set_message;
+
+ RESPONSE_FAILED:
+ {
+ save_errno = errno;
+ message = NULL;
+ /* Clear send_quit flag if needed. Do not set. */
+ sx->send_quit &= check_response(host, &save_errno, addrlist->more_errno,
+ sx->buffer, &code, &message, &pass_message);
+ goto FAILED;
+ }
+
+ SEND_FAILED:
+ {
+ save_errno = errno;
+ code = '4';
+ message = string_sprintf("smtp send to %s [%s] failed: %s",
+ host->name, host->address, message ? message : US strerror(save_errno));
+ sx->send_quit = FALSE;
+ goto FAILED;
+ }
+
+ FAILED:
+ {
+ BOOL message_error;
+
+ sx->ok = FALSE; /* For when reached by GOTO */
+ set_message = message;
+
+ /* We want to handle timeouts after MAIL or "." and loss of connection after
+ "." specially. They can indicate a problem with the sender address or with
+ the contents of the message rather than a real error on the connection. These
+ cases are treated in the same way as a 4xx response. This next bit of code
+ does the classification. */
+
+ switch(save_errno)
+ {
+ case 0:
+ case ERRNO_MAIL4XX:
+ case ERRNO_DATA4XX:
+ message_error = TRUE;
+ break;
+
+ case ETIMEDOUT:
+ message_error = Ustrncmp(smtp_command,"MAIL",4) == 0 ||
+ Ustrncmp(smtp_command,"end ",4) == 0;
+ break;
+
+ case ERRNO_SMTPCLOSED:
+ message_error = Ustrncmp(smtp_command,"end ",4) == 0;
+ break;
+
+#ifndef DISABLE_DKIM
+ case EACCES:
+ /* DKIM signing failure: avoid thinking we pipelined quit,
+ just abandon the message and close the socket. */
+
+ message_error = FALSE;
+# ifndef DISABLE_TLS
+ if (sx->cctx.tls_ctx)
+ {
+ tls_close(sx->cctx.tls_ctx,
+ sx->send_tlsclose ? TLS_SHUTDOWN_WAIT : TLS_SHUTDOWN_WONLY);
+ sx->cctx.tls_ctx = NULL;
+ }
+# endif
+ break;
+#endif
+ default:
+ message_error = FALSE;
+ break;
+ }
+
+ /* Handle the cases that are treated as message errors. These are:
+
+ (a) negative response or timeout after MAIL
+ (b) negative response after DATA
+ (c) negative response or timeout or dropped connection after "."
+ (d) utf8 support required and not offered
+
+ It won't be a negative response or timeout after RCPT, as that is dealt
+ with separately above. The action in all cases is to set an appropriate
+ error code for all the addresses, but to leave yield set to OK because the
+ host itself has not failed. Of course, it might in practice have failed
+ when we've had a timeout, but if so, we'll discover that at the next
+ delivery attempt. For a temporary error, set the message_defer flag, and
+ write to the logs for information if this is not the last host. The error
+ for the last host will be logged as part of the address's log line. */
+
+ if (message_error)
+ {
+ if (mua_wrapper) code = '5'; /* Force hard failure in wrapper mode */
+
+ /* If there's an errno, the message contains just the identity of
+ the host. */
+
+ if (code == '5')
+ set_rc = FAIL;
+ else /* Anything other than 5 is treated as temporary */
+ {
+ set_rc = DEFER;
+ if (save_errno > 0)
+ message = US string_sprintf("%s: %s", message, strerror(save_errno));
+
+ write_logs(host, message, sx->first_addr ? sx->first_addr->basic_errno : 0);
+
+ *message_defer = TRUE;
+ }
+#ifdef TIOCOUTQ
+ DEBUG(D_transport) if (sx->cctx.sock >= 0)
+ {
+ int n;
+ if (ioctl(sx->cctx.sock, TIOCOUTQ, &n) == 0)
+ debug_printf("%d bytes remain in socket output buffer\n", n);
+ }
+#endif
+ }
+ /* Otherwise, we have an I/O error or a timeout other than after MAIL or
+ ".", or some other transportation error. We defer all addresses and yield
+ DEFER, except for the case of failed add_headers expansion, or a transport
+ filter failure, when the yield should be ERROR, to stop it trying other
+ hosts. */
+
+ else
+ {
+#ifndef DISABLE_PIPE_CONNECT
+ /* If we were early-pipelinng and the actual EHLO response did not match
+ the cached value we assumed, we could have detected it and passed a
+ custom errno through to here. It would be nice to RSET and retry right
+ away, but to reliably do that we eould need an extra synch point before
+ we committed to data and that would discard half the gained roundrips.
+ Or we could summarily drop the TCP connection. but that is also ugly.
+ Instead, we ignore the possibility (having freshened the cache) and rely
+ on the server telling us with a nonmessage error if we have tried to
+ do something it no longer supports. */
+#endif
+ set_rc = DEFER;
+ yield = (save_errno == ERRNO_CHHEADER_FAIL ||
+ save_errno == ERRNO_FILTER_FAIL) ? ERROR : DEFER;
+ }
+ }
+
+ set_errno(addrlist, save_errno, set_message, set_rc, pass_message, host,
+#ifdef EXPERIMENTAL_DSN_INFO
+ sx->smtp_greeting, sx->helo_response,
+#endif
+ &sx->delivery_start);
+ }
+
+/* If all has gone well, send_quit will be set TRUE, implying we can end the
+SMTP session tidily. However, if there were too many addresses to send in one
+message (indicated by first_addr being non-NULL) we want to carry on with the
+rest of them. Also, it is desirable to send more than one message down the SMTP
+connection if there are several waiting, provided we haven't already sent so
+many as to hit the configured limit. The function transport_check_waiting looks
+for a waiting message and returns its id. Then transport_pass_socket tries to
+set up a continued delivery by passing the socket on to another process. The
+variable send_rset is FALSE if a message has just been successfully transferred.
+
+If we are already sending down a continued channel, there may be further
+addresses not yet delivered that are aimed at the same host, but which have not
+been passed in this run of the transport. In this case, continue_more will be
+true, and all we should do is send RSET if necessary, and return, leaving the
+channel open.
+
+However, if no address was disposed of, i.e. all addresses got 4xx errors, we
+do not want to continue with other messages down the same channel, because that
+can lead to looping between two or more messages, all with the same,
+temporarily failing address(es). [The retry information isn't updated yet, so
+new processes keep on trying.] We probably also don't want to try more of this
+message's addresses either.
+
+If we have started a TLS session, we have to end it before passing the
+connection to a new process. However, not all servers can handle this (Exim
+can), so we do not pass such a connection on if the host matches
+hosts_nopass_tls. */
+
+DEBUG(D_transport)
+ debug_printf("ok=%d send_quit=%d send_rset=%d continue_more=%d "
+ "yield=%d first_address is %sNULL\n", sx->ok, sx->send_quit,
+ sx->send_rset, f.continue_more, yield, sx->first_addr ? "not " : "");
+
+if (sx->completed_addr && sx->ok && sx->send_quit)
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+ if (mail_limit = continue_sequence >= sx->max_mail)
+ {
+ DEBUG(D_transport)
+ debug_printf("reached limit %u for MAILs per conn\n", sx->max_mail);
+ }
+ else
+#endif
+ {
+ smtp_compare_t t_compare =
+ {.tblock = tblock, .current_sender_address = sender_address};
+
+ if ( sx->first_addr /* more addrs for this message */
+ || f.continue_more /* more addrs for continued-host */
+ || tcw_done && tcw /* more messages for host */
+ || (
+#ifndef DISABLE_TLS
+ ( tls_out.active.sock < 0 && !continue_proxy_cipher
+ || verify_check_given_host(CUSS &ob->hosts_nopass_tls, host) != OK
+ )
+ &&
+#endif
+ transport_check_waiting(tblock->name, host->name,
+ sx->max_mail, new_message_id,
+ (oicf)smtp_are_same_identities, (void*)&t_compare)
+ ) )
+ {
+ uschar *msg;
+ BOOL pass_message;
+
+ if (sx->send_rset)
+ if (! (sx->ok = smtp_write_command(sx, SCMD_FLUSH, "RSET\r\n") >= 0))
+ {
+ msg = US string_sprintf("smtp send to %s [%s] failed: %s", host->name,
+ host->address, strerror(errno));
+ sx->send_quit = FALSE;
+ }
+ else if (! (sx->ok = smtp_read_response(sx, sx->buffer, sizeof(sx->buffer),
+ '2', ob->command_timeout)))
+ {
+ int code;
+ sx->send_quit = check_response(host, &errno, 0, sx->buffer, &code, &msg,
+ &pass_message);
+ if (!sx->send_quit)
+ {
+ DEBUG(D_transport) debug_printf("H=%s [%s] %s\n",
+ host->name, host->address, msg);
+ }
+ }
+
+ /* Either RSET was not needed, or it succeeded */
+
+ if (sx->ok)
+ {
+#ifndef DISABLE_TLS
+ int pfd[2];
+#endif
+ int socket_fd = sx->cctx.sock;
+
+ if (sx->first_addr) /* More addresses still to be sent */
+ { /* for this message */
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+ /* Any that we marked as skipped, reset to do now */
+ for (address_item * a = sx->first_addr; a; a = a->next)
+ if (a->transport_return == SKIP)
+ a->transport_return = PENDING_DEFER;
+#endif
+ continue_sequence++; /* for consistency */
+ clearflag(sx->first_addr, af_new_conn);
+ setflag(sx->first_addr, af_cont_conn); /* Causes * in logging */
+ pipelining_active = sx->pipelining_used; /* was cleared at DATA */
+ goto SEND_MESSAGE;
+ }
+
+ /* Unless caller said it already has more messages listed for this host,
+ pass the connection on to a new Exim process (below, the call to
+ transport_pass_socket). If the caller has more ready, just return with
+ the connection still open. */
+
+#ifndef DISABLE_TLS
+ if (tls_out.active.sock >= 0)
+ if ( f.continue_more
+ || verify_check_given_host(CUSS &ob->hosts_noproxy_tls, host) == OK)
+ {
+ /* Before passing the socket on, or returning to caller with it still
+ open, we must shut down TLS. Not all MTAs allow for the continuation
+ of the SMTP session when TLS is shut down. We test for this by sending
+ a new EHLO. If we don't get a good response, we don't attempt to pass
+ the socket on. */
+
+ tls_close(sx->cctx.tls_ctx,
+ sx->send_tlsclose ? TLS_SHUTDOWN_WAIT : TLS_SHUTDOWN_WONLY);
+ sx->send_tlsclose = FALSE;
+ sx->cctx.tls_ctx = NULL;
+ tls_out.active.sock = -1;
+ smtp_peer_options = smtp_peer_options_wrap;
+ sx->ok = !sx->smtps
+ && smtp_write_command(sx, SCMD_FLUSH, "EHLO %s\r\n", sx->helo_data)
+ >= 0
+ && smtp_read_response(sx, sx->buffer, sizeof(sx->buffer),
+ '2', ob->command_timeout);
+
+ if (sx->ok && f.continue_more)
+ goto TIDYUP; /* More addresses for another run */
+ }
+ else
+ {
+ /* Set up a pipe for proxying TLS for the new transport process */
+
+ smtp_peer_options |= OPTION_TLS;
+ if ((sx->ok = socketpair(AF_UNIX, SOCK_STREAM, 0, pfd) == 0))
+ socket_fd = pfd[1];
+ else
+ set_errno(sx->first_addr, errno, US"internal allocation problem",
+ DEFER, FALSE, host,
+# ifdef EXPERIMENTAL_DSN_INFO
+ sx->smtp_greeting, sx->helo_response,
+# endif
+ &sx->delivery_start);
+ }
+ else
+#endif
+ if (f.continue_more)
+ goto TIDYUP; /* More addresses for another run */
+
+ /* If the socket is successfully passed, we mustn't send QUIT (or
+ indeed anything!) from here. */
+
+ /*XXX DSN_INFO: assume likely to do new HELO; but for greet we'll want to
+ propagate it from the initial
+ */
+ if (sx->ok && transport_pass_socket(tblock->name, host->name,
+ host->address, new_message_id, socket_fd
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+ , sx->peer_limit_mail, sx->peer_limit_rcpt, sx->peer_limit_rcptdom
+#endif
+ ))
+ {
+ sx->send_quit = FALSE;
+
+ /* We have passed the client socket to a fresh transport process.
+ If TLS is still active, we need to proxy it for the transport we
+ just passed the baton to. Fork a child to to do it, and return to
+ get logging done asap. Which way to place the work makes assumptions
+ about post-fork prioritisation which may not hold on all platforms. */
+#ifndef DISABLE_TLS
+ if (tls_out.active.sock >= 0)
+ {
+ int pid = exim_fork(US"tls-proxy-interproc");
+ if (pid == 0) /* child; fork again to disconnect totally */
+ {
+ /* does not return */
+ smtp_proxy_tls(sx->cctx.tls_ctx, sx->buffer, sizeof(sx->buffer), pfd,
+ ob->command_timeout, host->name);
+ }
+
+ if (pid > 0) /* parent */
+ {
+ close(pfd[0]);
+ /* tidy the inter-proc to disconn the proxy proc */
+ waitpid(pid, NULL, 0);
+ tls_close(sx->cctx.tls_ctx, TLS_NO_SHUTDOWN);
+ sx->cctx.tls_ctx = NULL;
+ (void)close(sx->cctx.sock);
+ sx->cctx.sock = -1;
+ continue_transport = NULL;
+ continue_hostname = NULL;
+ goto TIDYUP;
+ }
+ log_write(0, LOG_PANIC_DIE, "fork failed");
+ }
+#endif
+ }
+ }
+
+ /* If RSET failed and there are addresses left, they get deferred. */
+ else
+ set_errno(sx->first_addr, errno, msg, DEFER, FALSE, host,
+#ifdef EXPERIMENTAL_DSN_INFO
+ sx->smtp_greeting, sx->helo_response,
+#endif
+ &sx->delivery_start);
+ }
+ }
+
+/* End off tidily with QUIT unless the connection has died or the socket has
+been passed to another process. */
+
+SEND_QUIT:
+if (sx->send_quit)
+ { /* Use _MORE to get QUIT in FIN segment */
+ (void)smtp_write_command(sx, SCMD_MORE, "QUIT\r\n");
+#ifndef DISABLE_TLS
+ if (sx->cctx.tls_ctx && sx->send_tlsclose)
+ {
+# ifdef EXIM_TCP_CORK /* Use _CORK to get TLS Close Notify in FIN segment */
+ (void) setsockopt(sx->cctx.sock, IPPROTO_TCP, EXIM_TCP_CORK, US &on, sizeof(on));
+# endif
+ tls_shutdown_wr(sx->cctx.tls_ctx);
+ sx->send_tlsclose = FALSE;
+ }
+#endif
+ }
+
+END_OFF:
+
+/* Close the socket, and return the appropriate value, first setting
+works because the NULL setting is passed back to the calling process, and
+remote_max_parallel is forced to 1 when delivering over an existing connection,
+
+If all went well and continue_more is set, we shouldn't actually get here if
+there are further addresses, as the return above will be taken. However,
+writing RSET might have failed, or there may be other addresses whose hosts are
+specified in the transports, and therefore not visible at top level, in which
+case continue_more won't get set. */
+
+if (sx->send_quit)
+ {
+ /* This flushes data queued in the socket, being the QUIT and any TLS Close,
+ sending them along with the client FIN flag. Us (we hope) sending FIN first
+ means we (client) take the TIME_WAIT state, so the server (which likely has a
+ higher connection rate) does not have to. */
+
+ HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" SMTP(shutdown)>>\n");
+ shutdown(sx->cctx.sock, SHUT_WR);
+ }
+
+if (sx->send_quit || tcw_done && !tcw)
+ {
+ /* Wait for (we hope) ack of our QUIT, and a server FIN. Discard any data
+ received, then discard the socket. Any packet received after then, or receive
+ data still in the socket, will get a RST - hence the pause/drain. */
+
+ /* Reap the response to QUIT, timing out after one second */
+ (void) smtp_read_response(sx, sx->buffer, sizeof(sx->buffer), '2', 1);
+#ifndef DISABLE_TLS
+ if (sx->cctx.tls_ctx)
+ {
+ int n;
+
+ /* Reap the TLS Close Notify from the server, timing out after one second */
+ sigalrm_seen = FALSE;
+ ALARM(1);
+ do
+ n = tls_read(sx->cctx.tls_ctx, sx->inbuffer, sizeof(sx->inbuffer));
+ while (!sigalrm_seen && n > 0);
+ ALARM_CLR(0);
+
+ if (sx->send_tlsclose)
+ {
+# ifdef EXIM_TCP_CORK
+ (void) setsockopt(sx->cctx.sock, IPPROTO_TCP, EXIM_TCP_CORK, US &on, sizeof(on));
+# endif
+ tls_close(sx->cctx.tls_ctx, TLS_SHUTDOWN_WAIT);
+ }
+ else
+ tls_close(sx->cctx.tls_ctx, TLS_SHUTDOWN_WONLY);
+ sx->cctx.tls_ctx = NULL;
+ }
+#endif
+
+ /* Drain any trailing data from the socket before close, to avoid sending a RST */
+
+ if ( poll_one_fd(sx->cctx.sock, POLLIN, 20) != 0 /* 20ms */
+ && fcntl(sx->cctx.sock, F_SETFL, O_NONBLOCK) == 0)
+ for (int i = 16, n; /* drain socket */
+ (n = read(sx->cctx.sock, sx->inbuffer, sizeof(sx->inbuffer))) > 0 && i > 0;
+ i--) HDEBUG(D_transport|D_acl|D_v)
+ {
+ int m = MIN(n, 64);
+ debug_printf_indent(" SMTP(drain %d bytes)<< %.*s\n", n, m, sx->inbuffer);
+ for (m = 0; m < n; m++)
+ debug_printf("0x%02x\n", sx->inbuffer[m]);
+ }
+ }
+HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" SMTP(close)>>\n");
+(void)close(sx->cctx.sock);
+sx->cctx.sock = -1;
+continue_transport = NULL;
+continue_hostname = NULL;
+smtp_debug_cmd_report();
+
+#ifndef DISABLE_EVENT
+(void) event_raise(tblock->event_action, US"tcp:close", NULL, NULL);
+#endif
+
+#ifdef SUPPORT_DANE
+if (dane_held)
+ {
+ sx->first_addr = NULL;
+ for (address_item * a = sx->addrlist->next; a; a = a->next)
+ if (a->transport_return == DANE)
+ {
+ a->transport_return = PENDING_DEFER;
+ if (!sx->first_addr)
+ {
+ /* Remember the new start-point in the addrlist, for smtp_setup_conn()
+ to get the domain string for SNI */
+
+ sx->first_addr = a;
+ clearflag(a, af_cont_conn);
+ setflag(a, af_new_conn); /* clear * from logging */
+ DEBUG(D_transport) debug_printf("DANE: go-around for %s\n", a->domain);
+ }
+ }
+ continue_sequence = 1; /* for consistency */
+ goto REPEAT_CONN;
+ }
+#endif
+
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+if (mail_limit && sx->first_addr)
+ {
+ /* Reset the sequence count since we closed the connection. This is flagged
+ on the pipe back to the delivery process so that a non-continued-conn delivery
+ is logged. */
+
+ continue_sequence = 1; /* for consistency */
+ clearflag(sx->first_addr, af_cont_conn);
+ setflag(sx->first_addr, af_new_conn); /* clear * from logging */
+ goto REPEAT_CONN;
+ }
+#endif
+
+return yield;
+
+TIDYUP:
+#ifdef SUPPORT_DANE
+if (dane_held) for (address_item * a = sx->addrlist->next; a; a = a->next)
+ if (a->transport_return == DANE)
+ a->transport_return = PENDING_DEFER;
+#endif
+return yield;
+}
+
+
+
+
+/*************************************************
+* Closedown entry point *
+*************************************************/
+
+/* This function is called when exim is passed an open smtp channel
+from another incarnation, but the message which it has been asked
+to deliver no longer exists. The channel is on stdin.
+
+We might do fancy things like looking for another message to send down
+the channel, but if the one we sought has gone, it has probably been
+delivered by some other process that itself will seek further messages,
+so just close down our connection.
+
+Argument: pointer to the transport instance block
+Returns: nothing
+*/
+
+void
+smtp_transport_closedown(transport_instance *tblock)
+{
+smtp_transport_options_block * ob = SOB tblock->options_block;
+client_conn_ctx cctx;
+smtp_context sx;
+uschar buffer[256];
+uschar inbuffer[4096];
+uschar outbuffer[16];
+
+/*XXX really we need an active-smtp-client ctx, rather than assuming stdout */
+cctx.sock = fileno(stdin);
+cctx.tls_ctx = cctx.sock == tls_out.active.sock ? tls_out.active.tls_ctx : NULL;
+
+sx.inblock.cctx = &cctx;
+sx.inblock.buffer = inbuffer;
+sx.inblock.buffersize = sizeof(inbuffer);
+sx.inblock.ptr = inbuffer;
+sx.inblock.ptrend = inbuffer;
+
+sx.outblock.cctx = &cctx;
+sx.outblock.buffersize = sizeof(outbuffer);
+sx.outblock.buffer = outbuffer;
+sx.outblock.ptr = outbuffer;
+sx.outblock.cmd_count = 0;
+sx.outblock.authenticating = FALSE;
+
+(void)smtp_write_command(&sx, SCMD_FLUSH, "QUIT\r\n");
+(void)smtp_read_response(&sx, buffer, sizeof(buffer), '2', ob->command_timeout);
+(void)close(cctx.sock);
+}
+
+
+
+/*************************************************
+* Prepare addresses for delivery *
+*************************************************/
+
+/* This function is called to flush out error settings from previous delivery
+attempts to other hosts. It also records whether we got here via an MX record
+or not in the more_errno field of the address. We are interested only in
+addresses that are still marked DEFER - others may have got delivered to a
+previously considered IP address. Set their status to PENDING_DEFER to indicate
+which ones are relevant this time.
+
+Arguments:
+ addrlist the list of addresses
+ host the host we are delivering to
+
+Returns: the first address for this delivery
+*/
+
+static address_item *
+prepare_addresses(address_item *addrlist, host_item *host)
+{
+address_item *first_addr = NULL;
+for (address_item * addr = addrlist; addr; addr = addr->next)
+ if (addr->transport_return == DEFER)
+ {
+ if (!first_addr) first_addr = addr;
+ addr->transport_return = PENDING_DEFER;
+ addr->basic_errno = 0;
+ addr->more_errno = (host->mx >= 0)? 'M' : 'A';
+ addr->message = NULL;
+#ifndef DISABLE_TLS
+ addr->cipher = NULL;
+ addr->ourcert = NULL;
+ addr->peercert = NULL;
+ addr->peerdn = NULL;
+ addr->ocsp = OCSP_NOT_REQ;
+ addr->tlsver = NULL;
+#endif
+#ifdef EXPERIMENTAL_DSN_INFO
+ addr->smtp_greeting = NULL;
+ addr->helo_response = NULL;
+#endif
+ }
+return first_addr;
+}
+
+
+
+/*************************************************
+* Main entry point *
+*************************************************/
+
+/* See local README for interface details. As this is a remote transport, it is
+given a chain of addresses to be delivered in one connection, if possible. It
+always returns TRUE, indicating that each address has its own independent
+status set, except if there is a setting up problem, in which case it returns
+FALSE. */
+
+BOOL
+smtp_transport_entry(
+ transport_instance *tblock, /* data for this instantiation */
+ address_item *addrlist) /* addresses we are working on */
+{
+int defport;
+int hosts_defer = 0;
+int hosts_fail = 0;
+int hosts_looked_up = 0;
+int hosts_retry = 0;
+int hosts_serial = 0;
+int hosts_total = 0;
+int total_hosts_tried = 0;
+BOOL expired = TRUE;
+uschar *expanded_hosts = NULL;
+uschar *pistring;
+uschar *tid = string_sprintf("%s transport", tblock->name);
+smtp_transport_options_block *ob = SOB tblock->options_block;
+host_item *hostlist = addrlist->host_list;
+host_item *host = NULL;
+
+DEBUG(D_transport)
+ {
+ debug_printf("%s transport entered\n", tblock->name);
+ for (address_item * addr = addrlist; addr; addr = addr->next)
+ debug_printf(" %s\n", addr->address);
+ if (hostlist)
+ {
+ debug_printf("hostlist:\n");
+ for (host_item * host = hostlist; host; host = host->next)
+ debug_printf(" '%s' IP %s port %d\n", host->name, host->address, host->port);
+ }
+ if (continue_hostname)
+ debug_printf("already connected to %s [%s] (on fd %d)\n",
+ continue_hostname, continue_host_address,
+ cutthrough.cctx.sock >= 0 ? cutthrough.cctx.sock : 0);
+ }
+
+/* Check the restrictions on line length */
+
+if (max_received_linelength > ob->message_linelength_limit)
+ {
+ struct timeval now;
+ gettimeofday(&now, NULL);
+
+ for (address_item * addr = addrlist; addr; addr = addr->next)
+ if (addr->transport_return == DEFER)
+ addr->transport_return = PENDING_DEFER;
+
+ set_errno_nohost(addrlist, ERRNO_SMTPFORMAT,
+ US"message has lines too long for transport", FAIL, TRUE, &now);
+ goto END_TRANSPORT;
+ }
+
+/* Set the flag requesting that these hosts be added to the waiting
+database if the delivery fails temporarily or if we are running with
+queue_smtp or a 2-stage queue run. This gets unset for certain
+kinds of error, typically those that are specific to the message. */
+
+update_waiting = TRUE;
+
+/* If a host list is not defined for the addresses - they must all have the
+same one in order to be passed to a single transport - or if the transport has
+a host list with hosts_override set, use the host list supplied with the
+transport. It is an error for this not to exist. */
+
+if (!hostlist || (ob->hosts_override && ob->hosts))
+ {
+ if (!ob->hosts)
+ {
+ addrlist->message = string_sprintf("%s transport called with no hosts set",
+ tblock->name);
+ addrlist->transport_return = PANIC;
+ return FALSE; /* Only top address has status */
+ }
+
+ DEBUG(D_transport) debug_printf("using the transport's hosts: %s\n",
+ ob->hosts);
+
+ /* If the transport's host list contains no '$' characters, and we are not
+ randomizing, it is fixed and therefore a chain of hosts can be built once
+ and for all, and remembered for subsequent use by other calls to this
+ transport. If, on the other hand, the host list does contain '$', or we are
+ randomizing its order, we have to rebuild it each time. In the fixed case,
+ as the hosts string will never be used again, it doesn't matter that we
+ replace all the : characters with zeros. */
+
+ if (!ob->hostlist)
+ {
+ uschar *s = ob->hosts;
+
+ if (Ustrchr(s, '$'))
+ {
+ if (!(expanded_hosts = expand_string(s)))
+ {
+ addrlist->message = string_sprintf("failed to expand list of hosts "
+ "\"%s\" in %s transport: %s", s, tblock->name, expand_string_message);
+ addrlist->transport_return = f.search_find_defer ? DEFER : PANIC;
+ return FALSE; /* Only top address has status */
+ }
+ DEBUG(D_transport) debug_printf("expanded list of hosts \"%s\" to "
+ "\"%s\"\n", s, expanded_hosts);
+ s = expanded_hosts;
+ }
+ else
+ if (ob->hosts_randomize) s = expanded_hosts = string_copy(s);
+
+ if (is_tainted(s))
+ {
+ log_write(0, LOG_MAIN|LOG_PANIC,
+ "attempt to use tainted host list '%s' from '%s' in transport %s",
+ s, ob->hosts, tblock->name);
+ /* Avoid leaking info to an attacker */
+ addrlist->message = US"internal configuration error";
+ addrlist->transport_return = PANIC;
+ return FALSE;
+ }
+
+ host_build_hostlist(&hostlist, s, ob->hosts_randomize);
+
+ /* Check that the expansion yielded something useful. */
+ if (!hostlist)
+ {
+ addrlist->message =
+ string_sprintf("%s transport has empty hosts setting", tblock->name);
+ addrlist->transport_return = PANIC;
+ return FALSE; /* Only top address has status */
+ }
+
+ /* If there was no expansion of hosts, save the host list for
+ next time. */
+
+ if (!expanded_hosts) ob->hostlist = hostlist;
+ }
+
+ /* This is not the first time this transport has been run in this delivery;
+ the host list was built previously. */
+
+ else
+ hostlist = ob->hostlist;
+ }
+
+/* The host list was supplied with the address. If hosts_randomize is set, we
+must sort it into a random order if it did not come from MX records and has not
+already been randomized (but don't bother if continuing down an existing
+connection). */
+
+else if (ob->hosts_randomize && hostlist->mx == MX_NONE && !continue_hostname)
+ {
+ host_item *newlist = NULL;
+ while (hostlist)
+ {
+ host_item *h = hostlist;
+ hostlist = hostlist->next;
+
+ h->sort_key = random_number(100);
+
+ if (!newlist)
+ {
+ h->next = NULL;
+ newlist = h;
+ }
+ else if (h->sort_key < newlist->sort_key)
+ {
+ h->next = newlist;
+ newlist = h;
+ }
+ else
+ {
+ host_item *hh = newlist;
+ while (hh->next)
+ {
+ if (h->sort_key < hh->next->sort_key) break;
+ hh = hh->next;
+ }
+ h->next = hh->next;
+ hh->next = h;
+ }
+ }
+
+ hostlist = addrlist->host_list = newlist;
+ }
+
+/* Sort out the default port. */
+
+if (!smtp_get_port(ob->port, addrlist, &defport, tid)) return FALSE;
+
+/* For each host-plus-IP-address on the list:
+
+. If this is a continued delivery and the host isn't the one with the
+ current connection, skip.
+
+. If the status is unusable (i.e. previously failed or retry checked), skip.
+
+. If no IP address set, get the address, either by turning the name into
+ an address, calling gethostbyname if gethostbyname is on, or by calling
+ the DNS. The DNS may yield multiple addresses, in which case insert the
+ extra ones into the list.
+
+. Get the retry data if not previously obtained for this address and set the
+ field which remembers the state of this address. Skip if the retry time is
+ not reached. If not, remember whether retry data was found. The retry string
+ contains both the name and the IP address.
+
+. Scan the list of addresses and mark those whose status is DEFER as
+ PENDING_DEFER. These are the only ones that will be processed in this cycle
+ of the hosts loop.
+
+. Make a delivery attempt - addresses marked PENDING_DEFER will be tried.
+ Some addresses may be successfully delivered, others may fail, and yet
+ others may get temporary errors and so get marked DEFER.
+
+. The return from the delivery attempt is OK if a connection was made and a
+ valid SMTP dialogue was completed. Otherwise it is DEFER.
+
+. If OK, add a "remove" retry item for this host/IPaddress, if any.
+
+. If fail to connect, or other defer state, add a retry item.
+
+. If there are any addresses whose status is still DEFER, carry on to the
+ next host/IPaddress, unless we have tried the number of hosts given
+ by hosts_max_try or hosts_max_try_hardlimit; otherwise return. Note that
+ there is some fancy logic for hosts_max_try that means its limit can be
+ overstepped in some circumstances.
+
+If we get to the end of the list, all hosts have deferred at least one address,
+or not reached their retry times. If delay_after_cutoff is unset, it requests a
+delivery attempt to those hosts whose last try was before the arrival time of
+the current message. To cope with this, we have to go round the loop a second
+time. After that, set the status and error data for any addresses that haven't
+had it set already. */
+
+for (int cutoff_retry = 0;
+ expired && cutoff_retry < (ob->delay_after_cutoff ? 1 : 2);
+ cutoff_retry++)
+ {
+ host_item *nexthost = NULL;
+ int unexpired_hosts_tried = 0;
+ BOOL continue_host_tried = FALSE;
+
+retry_non_continued:
+ for (host = hostlist;
+ host
+ && unexpired_hosts_tried < ob->hosts_max_try
+ && total_hosts_tried < ob->hosts_max_try_hardlimit;
+ host = nexthost)
+ {
+ int rc;
+ int host_af;
+ BOOL host_is_expired = FALSE;
+ BOOL message_defer = FALSE;
+ BOOL some_deferred = FALSE;
+ address_item *first_addr = NULL;
+ uschar *interface = NULL;
+ uschar *retry_host_key = NULL;
+ uschar *retry_message_key = NULL;
+ uschar *serialize_key = NULL;
+
+ /* Default next host is next host. :-) But this can vary if the
+ hosts_max_try limit is hit (see below). It may also be reset if a host
+ address is looked up here (in case the host was multihomed). */
+
+ nexthost = host->next;
+
+ /* If the address hasn't yet been obtained from the host name, look it up
+ now, unless the host is already marked as unusable. If it is marked as
+ unusable, it means that the router was unable to find its IP address (in
+ the DNS or wherever) OR we are in the 2nd time round the cutoff loop, and
+ the lookup failed last time. We don't get this far if *all* MX records
+ point to non-existent hosts; that is treated as a hard error.
+
+ We can just skip this host entirely. When the hosts came from the router,
+ the address will timeout based on the other host(s); when the address is
+ looked up below, there is an explicit retry record added.
+
+ Note that we mustn't skip unusable hosts if the address is not unset; they
+ may be needed as expired hosts on the 2nd time round the cutoff loop. */
+
+ if (!host->address)
+ {
+ int new_port, flags;
+
+ if (host->status >= hstatus_unusable)
+ {
+ DEBUG(D_transport) debug_printf("%s has no address and is unusable - skipping\n",
+ host->name);
+ continue;
+ }
+
+ DEBUG(D_transport) debug_printf("getting address for %s\n", host->name);
+
+ /* The host name is permitted to have an attached port. Find it, and
+ strip it from the name. Just remember it for now. */
+
+ new_port = host_item_get_port(host);
+
+ /* Count hosts looked up */
+
+ hosts_looked_up++;
+
+ /* Find by name if so configured, or if it's an IP address. We don't
+ just copy the IP address, because we need the test-for-local to happen. */
+
+ flags = HOST_FIND_BY_A | HOST_FIND_BY_AAAA;
+ if (ob->dns_qualify_single) flags |= HOST_FIND_QUALIFY_SINGLE;
+ if (ob->dns_search_parents) flags |= HOST_FIND_SEARCH_PARENTS;
+
+ if (ob->gethostbyname || string_is_ip_address(host->name, NULL) != 0)
+ rc = host_find_byname(host, NULL, flags, NULL, TRUE);
+ else
+ rc = host_find_bydns(host, NULL, flags, NULL, NULL, NULL,
+ &ob->dnssec, /* domains for request/require */
+ NULL, NULL);
+
+ /* Update the host (and any additional blocks, resulting from
+ multihoming) with a host-specific port, if any. */
+
+ for (host_item * hh = host; hh != nexthost; hh = hh->next) hh->port = new_port;
+
+ /* Failure to find the host at this time (usually DNS temporary failure)
+ is really a kind of routing failure rather than a transport failure.
+ Therefore we add a retry item of the routing kind, not to stop us trying
+ to look this name up here again, but to ensure the address gets timed
+ out if the failures go on long enough. A complete failure at this point
+ commonly points to a configuration error, but the best action is still
+ to carry on for the next host. */
+
+ if (rc == HOST_FIND_AGAIN || rc == HOST_FIND_SECURITY || rc == HOST_FIND_FAILED)
+ {
+ retry_add_item(addrlist, string_sprintf("R:%s", host->name), 0);
+ expired = FALSE;
+ if (rc == HOST_FIND_AGAIN) hosts_defer++; else hosts_fail++;
+ DEBUG(D_transport) debug_printf("rc = %s for %s\n", (rc == HOST_FIND_AGAIN)?
+ "HOST_FIND_AGAIN" : "HOST_FIND_FAILED", host->name);
+ host->status = hstatus_unusable;
+
+ for (address_item * addr = addrlist; addr; addr = addr->next)
+ {
+ if (addr->transport_return != DEFER) continue;
+ addr->basic_errno = ERRNO_UNKNOWNHOST;
+ addr->message = string_sprintf(
+ rc == HOST_FIND_SECURITY
+ ? "lookup of IP address for %s was insecure"
+ : "failed to lookup IP address for %s",
+ host->name);
+ }
+ continue;
+ }
+
+ /* If the host is actually the local host, we may have a problem, or
+ there may be some cunning configuration going on. In the problem case,
+ log things and give up. The default transport status is already DEFER. */
+
+ if (rc == HOST_FOUND_LOCAL && !ob->allow_localhost)
+ {
+ for (address_item * addr = addrlist; addr; addr = addr->next)
+ {
+ addr->basic_errno = ERRNO_HOST_IS_LOCAL;
+ addr->message = string_sprintf("%s transport found host %s to be "
+ "local", tblock->name, host->name);
+ }
+ goto END_TRANSPORT;
+ }
+ } /* End of block for IP address lookup */
+
+ /* If this is a continued delivery, we are interested only in the host
+ which matches the name of the existing open channel. The check is put
+ here after the local host lookup, in case the name gets expanded as a
+ result of the lookup. Set expired FALSE, to save the outer loop executing
+ twice. */
+
+ if (continue_hostname)
+ if ( Ustrcmp(continue_hostname, host->name) != 0
+ || Ustrcmp(continue_host_address, host->address) != 0
+ )
+ {
+ expired = FALSE;
+ continue; /* With next host */
+ }
+ else
+ continue_host_tried = TRUE;
+
+ /* Reset the default next host in case a multihomed host whose addresses
+ are not looked up till just above added to the host list. */
+
+ nexthost = host->next;
+
+ /* If queue_smtp is set (-odqs or the first part of a 2-stage run), or the
+ domain is in queue_smtp_domains, we don't actually want to attempt any
+ deliveries. When doing a queue run, queue_smtp_domains is always unset. If
+ there is a lookup defer in queue_smtp_domains, proceed as if the domain
+ were not in it. We don't want to hold up all SMTP deliveries! Except when
+ doing a two-stage queue run, don't do this if forcing. */
+
+ if ( (!f.deliver_force || f.queue_2stage)
+ && ( f.queue_smtp
+ || match_isinlist(addrlist->domain,
+ CUSS &queue_smtp_domains, 0,
+ &domainlist_anchor, NULL, MCL_DOMAIN, TRUE, NULL) == OK)
+ )
+ {
+ DEBUG(D_transport) debug_printf("first-pass routing only\n");
+ expired = FALSE;
+ for (address_item * addr = addrlist; addr; addr = addr->next)
+ if (addr->transport_return == DEFER)
+ addr->message = US"first-pass only routing due to -odqs, "
+ "queue_smtp_domains or control=queue";
+ continue; /* With next host */
+ }
+
+ /* Count hosts being considered - purely for an intelligent comment
+ if none are usable. */
+
+ hosts_total++;
+
+ /* Set $host and $host address now in case they are needed for the
+ interface expansion or the serialize_hosts check; they remain set if an
+ actual delivery happens. */
+
+ deliver_host = host->name;
+ deliver_host_address = host->address;
+ lookup_dnssec_authenticated = host->dnssec == DS_YES ? US"yes"
+ : host->dnssec == DS_NO ? US"no"
+ : US"";
+
+ /* Set up a string for adding to the retry key if the port number is not
+ the standard SMTP port. A host may have its own port setting that overrides
+ the default. */
+
+ pistring = string_sprintf(":%d", host->port == PORT_NONE
+ ? defport : host->port);
+ if (Ustrcmp(pistring, ":25") == 0) pistring = US"";
+
+ /* Select IPv4 or IPv6, and choose an outgoing interface. If the interface
+ string is set, even if constant (as different transports can have different
+ constant settings), we must add it to the key that is used for retries,
+ because connections to the same host from a different interface should be
+ treated separately. */
+
+ host_af = Ustrchr(host->address, ':') ? AF_INET6 : AF_INET;
+ {
+ uschar * s = ob->interface;
+ if (s && *s)
+ {
+ if (!smtp_get_interface(s, host_af, addrlist, &interface, tid))
+ return FALSE;
+ pistring = string_sprintf("%s/%s", pistring, interface);
+ }
+ }
+
+ /* The first time round the outer loop, check the status of the host by
+ inspecting the retry data. The second time round, we are interested only
+ in expired hosts that haven't been tried since this message arrived. */
+
+ if (cutoff_retry == 0)
+ {
+ BOOL incl_ip;
+ /* Ensure the status of the address is set by checking retry data if
+ necessary. There may be host-specific retry data (applicable to all
+ messages) and also data for retries of a specific message at this host.
+ If either of these retry records are actually read, the keys used are
+ returned to save recomputing them later. */
+
+ if (exp_bool(addrlist, US"transport", tblock->name, D_transport,
+ US"retry_include_ip_address", ob->retry_include_ip_address,
+ ob->expand_retry_include_ip_address, &incl_ip) != OK)
+ continue; /* with next host */
+
+ host_is_expired = retry_check_address(addrlist->domain, host, pistring,
+ incl_ip, &retry_host_key, &retry_message_key);
+
+ DEBUG(D_transport) debug_printf("%s [%s]%s retry-status = %s\n", host->name,
+ host->address ? host->address : US"", pistring,
+ host->status == hstatus_usable ? "usable"
+ : host->status == hstatus_unusable ? "unusable"
+ : host->status == hstatus_unusable_expired ? "unusable (expired)" : "?");
+
+ /* Skip this address if not usable at this time, noting if it wasn't
+ actually expired, both locally and in the address. */
+
+ switch (host->status)
+ {
+ case hstatus_unusable:
+ expired = FALSE;
+ setflag(addrlist, af_retry_skipped);
+ /* Fall through */
+
+ case hstatus_unusable_expired:
+ switch (host->why)
+ {
+ case hwhy_retry: hosts_retry++; break;
+ case hwhy_failed: hosts_fail++; break;
+ case hwhy_insecure:
+ case hwhy_deferred: hosts_defer++; break;
+ }
+
+ /* If there was a retry message key, implying that previously there
+ was a message-specific defer, we don't want to update the list of
+ messages waiting for these hosts. */
+
+ if (retry_message_key) update_waiting = FALSE;
+ continue; /* With the next host or IP address */
+ }
+ }
+
+ /* Second time round the loop: if the address is set but expired, and
+ the message is newer than the last try, let it through. */
+
+ else
+ {
+ if ( !host->address
+ || host->status != hstatus_unusable_expired
+ || host->last_try > received_time.tv_sec)
+ continue;
+ DEBUG(D_transport) debug_printf("trying expired host %s [%s]%s\n",
+ host->name, host->address, pistring);
+ host_is_expired = TRUE;
+ }
+
+ /* Setting "expired=FALSE" doesn't actually mean not all hosts are expired;
+ it remains TRUE only if all hosts are expired and none are actually tried.
+ */
+
+ expired = FALSE;
+
+ /* If this host is listed as one to which access must be serialized,
+ see if another Exim process has a connection to it, and if so, skip
+ this host. If not, update the database to record our connection to it
+ and remember this for later deletion. Do not do any of this if we are
+ sending the message down a pre-existing connection. */
+
+ if ( !continue_hostname
+ && verify_check_given_host(CUSS &ob->serialize_hosts, host) == OK)
+ {
+ serialize_key = string_sprintf("host-serialize-%s", host->name);
+ if (!enq_start(serialize_key, 1))
+ {
+ DEBUG(D_transport)
+ debug_printf("skipping host %s because another Exim process "
+ "is connected to it\n", host->name);
+ hosts_serial++;
+ continue;
+ }
+ }
+
+ /* OK, we have an IP address that is not waiting for its retry time to
+ arrive (it might be expired) OR (second time round the loop) we have an
+ expired host that hasn't been tried since the message arrived. Have a go
+ at delivering the message to it. First prepare the addresses by flushing
+ out the result of previous attempts, and finding the first address that
+ is still to be delivered. */
+
+ first_addr = prepare_addresses(addrlist, host);
+
+ DEBUG(D_transport) debug_printf("delivering %s to %s [%s] (%s%s)\n",
+ message_id, host->name, host->address, addrlist->address,
+ addrlist->next ? ", ..." : "");
+
+ set_process_info("delivering %s to %s [%s]%s (%s%s)",
+ message_id, host->name, host->address, pistring, addrlist->address,
+ addrlist->next ? ", ..." : "");
+
+ /* This is not for real; don't do the delivery. If there are
+ any remaining hosts, list them. */
+
+ if (f.dont_deliver)
+ {
+ struct timeval now;
+ gettimeofday(&now, NULL);
+ set_errno_nohost(addrlist, 0, NULL, OK, FALSE, &now);
+ for (address_item * addr = addrlist; addr; addr = addr->next)
+ {
+ addr->host_used = host;
+ addr->special_action = '*';
+ addr->message = US"delivery bypassed by -N option";
+ }
+ DEBUG(D_transport)
+ {
+ debug_printf("*** delivery by %s transport bypassed by -N option\n"
+ "*** host and remaining hosts:\n", tblock->name);
+ for (host_item * host2 = host; host2; host2 = host2->next)
+ debug_printf(" %s [%s]\n", host2->name,
+ host2->address ? host2->address : US"unset");
+ }
+ rc = OK;
+ }
+
+ /* This is for real. If the host is expired, we don't count it for
+ hosts_max_retry. This ensures that all hosts must expire before an address
+ is timed out, unless hosts_max_try_hardlimit (which protects against
+ lunatic DNS configurations) is reached.
+
+ If the host is not expired and we are about to hit the hosts_max_retry
+ limit, check to see if there is a subsequent hosts with a different MX
+ value. If so, make that the next host, and don't count this one. This is a
+ heuristic to make sure that different MXs do get tried. With a normal kind
+ of retry rule, they would get tried anyway when the earlier hosts were
+ delayed, but if the domain has a "retry every time" type of rule - as is
+ often used for the the very large ISPs, that won't happen. */
+
+ else
+ {
+ host_item * thost;
+ /* Make a copy of the host if it is local to this invocation
+ of the transport. */
+
+ if (expanded_hosts)
+ {
+ thost = store_get(sizeof(host_item), GET_UNTAINTED);
+ *thost = *host;
+ thost->name = string_copy(host->name);
+ thost->address = string_copy(host->address);
+ }
+ else
+ thost = host;
+
+ if (!host_is_expired && ++unexpired_hosts_tried >= ob->hosts_max_try)
+ {
+ DEBUG(D_transport)
+ debug_printf("hosts_max_try limit reached with this host\n");
+ for (host_item * h = host; h; h = h->next) if (h->mx != host->mx)
+ {
+ nexthost = h;
+ unexpired_hosts_tried--;
+ DEBUG(D_transport) debug_printf("however, a higher MX host exists "
+ "and will be tried\n");
+ break;
+ }
+ }
+
+ /* Attempt the delivery. */
+
+ total_hosts_tried++;
+ rc = smtp_deliver(addrlist, thost, host_af, defport, interface, tblock,
+ &message_defer, FALSE);
+
+ /* Yield is one of:
+ OK => connection made, each address contains its result;
+ message_defer is set for message-specific defers (when all
+ recipients are marked defer)
+ DEFER => there was a non-message-specific delivery problem;
+ ERROR => there was a problem setting up the arguments for a filter,
+ or there was a problem with expanding added headers
+ */
+
+ /* If the result is not OK, there was a non-message-specific problem.
+ If the result is DEFER, we need to write to the logs saying what happened
+ for this particular host, except in the case of authentication and TLS
+ failures, where the log has already been written. If all hosts defer a
+ general message is written at the end. */
+
+ if (rc == DEFER && first_addr->basic_errno != ERRNO_AUTHFAIL
+ && first_addr->basic_errno != ERRNO_TLSFAILURE)
+ write_logs(host, first_addr->message, first_addr->basic_errno);
+
+#ifndef DISABLE_EVENT
+ if (rc == DEFER)
+ deferred_event_raise(first_addr, host, US"msg:host:defer");
+#endif
+
+ /* If STARTTLS was accepted, but there was a failure in setting up the
+ TLS session (usually a certificate screwup), and the host is not in
+ hosts_require_tls, and tls_tempfail_tryclear is true, try again, with
+ TLS forcibly turned off. We have to start from scratch with a new SMTP
+ connection. That's why the retry is done from here, not from within
+ smtp_deliver(). [Rejections of STARTTLS itself don't screw up the
+ session, so the in-clear transmission after those errors, if permitted,
+ happens inside smtp_deliver().] */
+
+#ifndef DISABLE_TLS
+ if ( rc == DEFER
+ && first_addr->basic_errno == ERRNO_TLSFAILURE
+ && ob->tls_tempfail_tryclear
+ && verify_check_given_host(CUSS &ob->hosts_require_tls, host) != OK
+ )
+ {
+ log_write(0, LOG_MAIN,
+ "%s: delivering unencrypted to H=%s [%s] (not in hosts_require_tls)",
+ first_addr->message, host->name, host->address);
+ first_addr = prepare_addresses(addrlist, host);
+ rc = smtp_deliver(addrlist, thost, host_af, defport, interface, tblock,
+ &message_defer, TRUE);
+ if (rc == DEFER && first_addr->basic_errno != ERRNO_AUTHFAIL)
+ write_logs(host, first_addr->message, first_addr->basic_errno);
+# ifndef DISABLE_EVENT
+ if (rc == DEFER)
+ deferred_event_raise(first_addr, host, US"msg:host:defer");
+# endif
+ }
+#endif /*DISABLE_TLS*/
+
+#ifndef DISABLE_EVENT
+ /* If the last host gave a defer raise a per-message event */
+
+ if ( !( nexthost
+ && unexpired_hosts_tried < ob->hosts_max_try
+ && total_hosts_tried < ob->hosts_max_try_hardlimit
+ )
+ && (message_defer || rc == DEFER)
+ )
+ deferred_event_raise(first_addr, host, US"msg:defer");
+#endif
+ }
+
+ /* Delivery attempt finished */
+
+ set_process_info("delivering %s: just tried %s [%s]%s for %s%s: result %s",
+ message_id, host->name, host->address, pistring, addrlist->address,
+ addrlist->next ? " (& others)" : "", rc_to_string(rc));
+
+ /* Release serialization if set up */
+
+ if (serialize_key) enq_end(serialize_key);
+
+ /* If the result is DEFER, or if a host retry record is known to exist, we
+ need to add an item to the retry chain for updating the retry database
+ at the end of delivery. We only need to add the item to the top address,
+ of course. Also, if DEFER, we mark the IP address unusable so as to skip it
+ for any other delivery attempts using the same address. (It is copied into
+ the unusable tree at the outer level, so even if different address blocks
+ contain the same address, it still won't get tried again.) */
+
+ if (rc == DEFER || retry_host_key)
+ {
+ int delete_flag = rc != DEFER ? rf_delete : 0;
+ if (!retry_host_key)
+ {
+ BOOL incl_ip;
+ if (exp_bool(addrlist, US"transport", tblock->name, D_transport,
+ US"retry_include_ip_address", ob->retry_include_ip_address,
+ ob->expand_retry_include_ip_address, &incl_ip) != OK)
+ incl_ip = TRUE; /* error; use most-specific retry record */
+
+ retry_host_key = incl_ip
+ ? string_sprintf("T:%S:%s%s", host->name, host->address, pistring)
+ : string_sprintf("T:%S%s", host->name, pistring);
+ }
+
+ /* If a delivery of another message over an existing SMTP connection
+ yields DEFER, we do NOT set up retry data for the host. This covers the
+ case when there are delays in routing the addresses in the second message
+ that are so long that the server times out. This is alleviated by not
+ routing addresses that previously had routing defers when handling an
+ existing connection, but even so, this case may occur (e.g. if a
+ previously happily routed address starts giving routing defers). If the
+ host is genuinely down, another non-continued message delivery will
+ notice it soon enough. */
+
+ if (delete_flag != 0 || !continue_hostname)
+ retry_add_item(first_addr, retry_host_key, rf_host | delete_flag);
+
+ /* We may have tried an expired host, if its retry time has come; ensure
+ the status reflects the expiry for the benefit of any other addresses. */
+
+ if (rc == DEFER)
+ {
+ host->status = host_is_expired
+ ? hstatus_unusable_expired : hstatus_unusable;
+ host->why = hwhy_deferred;
+ }
+ }
+
+ /* If message_defer is set (host was OK, but every recipient got deferred
+ because of some message-specific problem), or if that had happened
+ previously so that a message retry key exists, add an appropriate item
+ to the retry chain. Note that if there was a message defer but now there is
+ a host defer, the message defer record gets deleted. That seems perfectly
+ reasonable. Also, stop the message from being remembered as waiting
+ for specific hosts. */
+
+ if (message_defer || retry_message_key)
+ {
+ int delete_flag = message_defer ? 0 : rf_delete;
+ if (!retry_message_key)
+ {
+ BOOL incl_ip;
+ if (exp_bool(addrlist, US"transport", tblock->name, D_transport,
+ US"retry_include_ip_address", ob->retry_include_ip_address,
+ ob->expand_retry_include_ip_address, &incl_ip) != OK)
+ incl_ip = TRUE; /* error; use most-specific retry record */
+
+ retry_message_key = incl_ip
+ ? string_sprintf("T:%S:%s%s:%s", host->name, host->address, pistring,
+ message_id)
+ : string_sprintf("T:%S%s:%s", host->name, pistring, message_id);
+ }
+ retry_add_item(addrlist, retry_message_key,
+ rf_message | rf_host | delete_flag);
+ update_waiting = FALSE;
+ }
+
+ /* Any return other than DEFER (that is, OK or ERROR) means that the
+ addresses have got their final statuses filled in for this host. In the OK
+ case, see if any of them are deferred. */
+
+ if (rc == OK)
+ for (address_item * addr = addrlist; addr; addr = addr->next)
+ if (addr->transport_return == DEFER)
+ {
+ some_deferred = TRUE;
+ break;
+ }
+
+ /* If no addresses deferred or the result was ERROR, return. We do this for
+ ERROR because a failing filter set-up or add_headers expansion is likely to
+ fail for any host we try. */
+
+ if (rc == ERROR || (rc == OK && !some_deferred))
+ {
+ DEBUG(D_transport) debug_printf("Leaving %s transport\n", tblock->name);
+ return TRUE; /* Each address has its status */
+ }
+
+ /* If the result was DEFER or some individual addresses deferred, let
+ the loop run to try other hosts with the deferred addresses, except for the
+ case when we were trying to deliver down an existing channel and failed.
+ Don't try any other hosts in this case. */
+
+ if (continue_hostname) break;
+
+ /* If the whole delivery, or some individual addresses, were deferred and
+ there are more hosts that could be tried, do not count this host towards
+ the hosts_max_try limit if the age of the message is greater than the
+ maximum retry time for this host. This means we may try try all hosts,
+ ignoring the limit, when messages have been around for some time. This is
+ important because if we don't try all hosts, the address will never time
+ out. NOTE: this does not apply to hosts_max_try_hardlimit. */
+
+ if ((rc == DEFER || some_deferred) && nexthost)
+ {
+ BOOL timedout;
+ retry_config *retry = retry_find_config(host->name, NULL, 0, 0);
+
+ if (retry && retry->rules)
+ {
+ retry_rule *last_rule;
+ for (last_rule = retry->rules;
+ last_rule->next;
+ last_rule = last_rule->next);
+ timedout = time(NULL) - received_time.tv_sec > last_rule->timeout;
+ }
+ else timedout = TRUE; /* No rule => timed out */
+
+ if (timedout)
+ {
+ unexpired_hosts_tried--;
+ DEBUG(D_transport) debug_printf("temporary delivery error(s) override "
+ "hosts_max_try (message older than host's retry time)\n");
+ }
+ }
+
+ DEBUG(D_transport)
+ {
+ if (unexpired_hosts_tried >= ob->hosts_max_try)
+ debug_printf("reached transport hosts_max_try limit %d\n",
+ ob->hosts_max_try);
+ if (total_hosts_tried >= ob->hosts_max_try_hardlimit)
+ debug_printf("reached transport hosts_max_try_hardlimit limit %d\n",
+ ob->hosts_max_try_hardlimit);
+ }
+
+ testharness_pause_ms(500); /* let server debug out */
+ } /* End of loop for trying multiple hosts. */
+
+ /* If we failed to find a matching host in the list, for an already-open
+ connection, just close it and start over with the list. This can happen
+ for routing that changes from run to run, or big multi-IP sites with
+ round-robin DNS. */
+
+ if (continue_hostname && !continue_host_tried)
+ {
+ int fd = cutthrough.cctx.sock >= 0 ? cutthrough.cctx.sock : 0;
+
+ DEBUG(D_transport) debug_printf("no hosts match already-open connection\n");
+#ifndef DISABLE_TLS
+ /* A TLS conn could be open for a cutthrough, but not for a plain continued-
+ transport */
+/*XXX doublecheck that! */
+
+ if (cutthrough.cctx.sock >= 0 && cutthrough.is_tls)
+ {
+ (void) tls_write(cutthrough.cctx.tls_ctx, US"QUIT\r\n", 6, FALSE);
+ tls_close(cutthrough.cctx.tls_ctx, TLS_SHUTDOWN_NOWAIT);
+ cutthrough.cctx.tls_ctx = NULL;
+ cutthrough.is_tls = FALSE;
+ }
+ else
+#else
+ (void) write(fd, US"QUIT\r\n", 6);
+#endif
+ (void) close(fd);
+ cutthrough.cctx.sock = -1;
+ continue_hostname = NULL;
+ goto retry_non_continued;
+ }
+
+ /* This is the end of the loop that repeats iff expired is TRUE and
+ ob->delay_after_cutoff is FALSE. The second time round we will
+ try those hosts that haven't been tried since the message arrived. */
+
+ DEBUG(D_transport)
+ {
+ debug_printf("all IP addresses skipped or deferred at least one address\n");
+ if (expired && !ob->delay_after_cutoff && cutoff_retry == 0)
+ debug_printf("retrying IP addresses not tried since message arrived\n");
+ }
+ }
+
+
+/* Get here if all IP addresses are skipped or defer at least one address. In
+MUA wrapper mode, this will happen only for connection or other non-message-
+specific failures. Force the delivery status for all addresses to FAIL. */
+
+if (mua_wrapper)
+ {
+ for (address_item * addr = addrlist; addr; addr = addr->next)
+ addr->transport_return = FAIL;
+ goto END_TRANSPORT;
+ }
+
+/* In the normal, non-wrapper case, add a standard message to each deferred
+address if there hasn't been an error, that is, if it hasn't actually been
+tried this time. The variable "expired" will be FALSE if any deliveries were
+actually tried, or if there was at least one host that was not expired. That
+is, it is TRUE only if no deliveries were tried and all hosts were expired. If
+a delivery has been tried, an error code will be set, and the failing of the
+message is handled by the retry code later.
+
+If queue_smtp is set, or this transport was called to send a subsequent message
+down an existing TCP/IP connection, and something caused the host not to be
+found, we end up here, but can detect these cases and handle them specially. */
+
+for (address_item * addr = addrlist; addr; addr = addr->next)
+ {
+ /* If host is not NULL, it means that we stopped processing the host list
+ because of hosts_max_try or hosts_max_try_hardlimit. In the former case, this
+ means we need to behave as if some hosts were skipped because their retry
+ time had not come. Specifically, this prevents the address from timing out.
+ However, if we have hit hosts_max_try_hardlimit, we want to behave as if all
+ hosts were tried. */
+
+ if (host)
+ if (total_hosts_tried >= ob->hosts_max_try_hardlimit)
+ {
+ DEBUG(D_transport)
+ debug_printf("hosts_max_try_hardlimit reached: behave as if all "
+ "hosts were tried\n");
+ }
+ else
+ {
+ DEBUG(D_transport)
+ debug_printf("hosts_max_try limit caused some hosts to be skipped\n");
+ setflag(addr, af_retry_skipped);
+ }
+
+ if (f.queue_smtp) /* no deliveries attempted */
+ {
+ addr->transport_return = DEFER;
+ addr->basic_errno = 0;
+ addr->message = US"SMTP delivery explicitly queued";
+ }
+
+ else if ( addr->transport_return == DEFER
+ && (addr->basic_errno == ERRNO_UNKNOWNERROR || addr->basic_errno == 0)
+ && !addr->message
+ )
+ {
+ addr->basic_errno = ERRNO_HRETRY;
+ if (continue_hostname)
+ addr->message = US"no host found for existing SMTP connection";
+ else if (expired)
+ {
+ setflag(addr, af_pass_message); /* This is not a security risk */
+ addr->message = string_sprintf(
+ "all hosts%s have been failing for a long time %s",
+ addr->domain ? string_sprintf(" for '%s'", addr->domain) : US"",
+ ob->delay_after_cutoff
+ ? US"(and retry time not reached)"
+ : US"and were last tried after this message arrived");
+
+ /* If we are already using fallback hosts, or there are no fallback hosts
+ defined, convert the result to FAIL to cause a bounce. */
+
+ if (addr->host_list == addr->fallback_hosts || !addr->fallback_hosts)
+ addr->transport_return = FAIL;
+ }
+ else
+ {
+ const char * s;
+ if (hosts_retry == hosts_total)
+ s = "retry time not reached for any host%s";
+ else if (hosts_fail == hosts_total)
+ s = "all host address lookups%s failed permanently";
+ else if (hosts_defer == hosts_total)
+ s = "all host address lookups%s failed temporarily";
+ else if (hosts_serial == hosts_total)
+ s = "connection limit reached for all hosts%s";
+ else if (hosts_fail+hosts_defer == hosts_total)
+ s = "all host address lookups%s failed";
+ else
+ s = "some host address lookups failed and retry time "
+ "not reached for other hosts or connection limit reached%s";
+
+ addr->message = string_sprintf(s,
+ addr->domain ? string_sprintf(" for '%s'", addr->domain) : US"");
+ }
+ }
+ }
+
+/* Update the database which keeps information about which messages are waiting
+for which hosts to become available. For some message-specific errors, the
+update_waiting flag is turned off because we don't want follow-on deliveries in
+those cases. If this transport instance is explicitly limited to one message
+per connection then follow-on deliveries are not possible and there's no need
+to create/update the per-transport wait-<transport_name> database. */
+
+if (update_waiting && tblock->connection_max_messages != 1)
+ transport_update_waiting(hostlist, tblock->name);
+
+END_TRANSPORT:
+
+DEBUG(D_transport) debug_printf("Leaving %s transport\n", tblock->name);
+
+return TRUE; /* Each address has its status */
+}
+
+#endif /*!MACRO_PREDEF*/
+/* vi: aw ai sw=2
+*/
+/* End of transport/smtp.c */
diff --git a/src/transports/smtp.h b/src/transports/smtp.h
new file mode 100644
index 0000000..319e849
--- /dev/null
+++ b/src/transports/smtp.h
@@ -0,0 +1,252 @@
+/*************************************************
+* 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. */
+
+#define DELIVER_BUFFER_SIZE 4096
+
+#define PENDING 256
+#define PENDING_DEFER (PENDING + DEFER)
+#define PENDING_OK (PENDING + OK)
+
+
+#ifndef DISABLE_TLS
+/* Flags structure for validity of TLS configuration */
+
+typedef struct {
+ BOOL conn_certs:1; /* certificates etc. loaded */
+ BOOL cabundle:1; /* CA certificates loaded */
+ BOOL crl:1; /* CRL loaded */
+ BOOL pri_string:1; /* cipher priority-string cache loaded */
+ BOOL dh:1; /* Diffie-Helman params loaded */
+ BOOL ecdh:1; /* EC Diffie-Helman params loaded */
+
+ BOOL ca_rdn_emulate:1; /* do not advertise usable-cert list */
+ BOOL ocsp_hook:1; /* need hshake callback on session */
+
+ void * libdata0; /* library-dependent preloaded data */
+ void * libdata1; /* library-dependent preloaded data */
+} exim_tlslib_state;
+#endif
+
+
+/* Private structure for the private options and other private data. */
+
+typedef struct {
+ uschar *hosts;
+ uschar *fallback_hosts;
+ host_item *hostlist;
+ host_item *fallback_hostlist;
+ uschar *authenticated_sender;
+ uschar *helo_data;
+ uschar *interface;
+ uschar *port;
+ uschar *protocol;
+ uschar *dscp;
+ uschar *serialize_hosts;
+ uschar *hosts_try_auth;
+ uschar *hosts_require_alpn;
+ uschar *hosts_require_auth;
+ uschar *hosts_try_chunking;
+#ifdef SUPPORT_DANE
+ uschar *hosts_try_dane;
+ uschar *hosts_require_dane;
+ uschar *dane_require_tls_ciphers;
+#endif
+ uschar *hosts_try_fastopen;
+#ifndef DISABLE_PRDR
+ uschar *hosts_try_prdr;
+#endif
+#ifndef DISABLE_OCSP
+ uschar *hosts_request_ocsp;
+ uschar *hosts_require_ocsp;
+#endif
+ uschar *hosts_require_tls;
+ uschar *hosts_avoid_tls;
+ uschar *hosts_verify_avoid_tls;
+ uschar *hosts_avoid_pipelining;
+#ifndef DISABLE_PIPE_CONNECT
+ uschar *hosts_pipe_connect;
+#endif
+ uschar *hosts_avoid_esmtp;
+#ifndef DISABLE_TLS
+ uschar *hosts_nopass_tls;
+ uschar *hosts_noproxy_tls;
+#endif
+ int command_timeout;
+ int connect_timeout;
+ int data_timeout;
+ int final_timeout;
+ int size_addition;
+ int hosts_max_try;
+ int hosts_max_try_hardlimit;
+ int message_linelength_limit;
+ BOOL address_retry_include_sender;
+ BOOL allow_localhost;
+ BOOL authenticated_sender_force;
+ BOOL gethostbyname;
+ BOOL dns_qualify_single;
+ BOOL dns_search_parents;
+ dnssec_domains dnssec;
+ BOOL delay_after_cutoff;
+ BOOL hosts_override;
+ BOOL hosts_randomize;
+ BOOL keepalive;
+ BOOL lmtp_ignore_quota;
+ uschar *expand_retry_include_ip_address;
+ BOOL retry_include_ip_address;
+#ifdef SUPPORT_SOCKS
+ uschar *socks_proxy;
+#endif
+#ifndef DISABLE_TLS
+ uschar *tls_alpn;
+ uschar *tls_certificate;
+ uschar *tls_crl;
+ uschar *tls_privatekey;
+ uschar *tls_require_ciphers;
+# ifndef DISABLE_TLS_RESUME
+ uschar *host_name_extract;
+ uschar *tls_resumption_hosts;
+# endif
+ const uschar *tls_sni;
+ uschar *tls_verify_certificates;
+ int tls_dh_min_bits;
+ BOOL tls_tempfail_tryclear;
+ uschar *tls_verify_hosts;
+ uschar *tls_try_verify_hosts;
+ uschar *tls_verify_cert_hostnames;
+#endif
+#ifdef SUPPORT_I18N
+ uschar *utf8_downconvert;
+#endif
+#ifndef DISABLE_DKIM
+ struct ob_dkim dkim;
+#endif
+#ifdef EXPERIMENTAL_ARC
+ uschar *arc_sign;
+#endif
+#ifndef DISABLE_TLS
+ exim_tlslib_state tls_preload;
+#endif
+} smtp_transport_options_block;
+
+#define SOB (smtp_transport_options_block *)
+
+
+/* smtp connect context */
+typedef struct {
+ uschar * from_addr;
+ address_item * addrlist;
+
+ smtp_connect_args conn_args;
+ int port;
+
+ BOOL verify:1;
+ BOOL lmtp:1;
+ BOOL smtps:1;
+ BOOL ok:1;
+ BOOL setting_up:1;
+#ifndef DISABLE_PIPE_CONNECT
+ BOOL early_pipe_ok:1;
+ BOOL early_pipe_active:1;
+#endif
+ BOOL esmtp:1;
+ BOOL esmtp_sent:1;
+ BOOL pipelining_used:1;
+#ifndef DISABLE_PRDR
+ BOOL prdr_active:1;
+#endif
+#ifdef SUPPORT_I18N
+ BOOL utf8_needed:1;
+#endif
+ BOOL dsn_all_lasthop:1;
+#if !defined(DISABLE_TLS) && defined(SUPPORT_DANE)
+ BOOL dane_required:1;
+#endif
+#ifndef DISABLE_PIPE_CONNECT
+ BOOL pending_BANNER:1;
+ BOOL pending_EHLO:1;
+#endif
+ BOOL pending_MAIL:1;
+ BOOL pending_BDAT:1;
+ BOOL RCPT_452:1;
+ BOOL good_RCPT:1;
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+ BOOL single_rcpt_domain:1;
+#endif
+ BOOL completed_addr:1;
+ BOOL send_rset:1;
+ BOOL send_quit:1;
+ BOOL send_tlsclose:1;
+
+ unsigned peer_offered;
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+ unsigned peer_limit_mail;
+ unsigned peer_limit_rcpt;
+ unsigned peer_limit_rcptdom;
+#endif
+
+ unsigned max_mail;
+ int max_rcpt;
+ int cmd_count;
+
+ unsigned avoid_option;
+ uschar * igquotstr;
+ uschar * helo_data;
+#ifdef EXPERIMENTAL_DSN_INFO
+ uschar * smtp_greeting;
+ uschar * helo_response;
+#endif
+#ifndef DISABLE_PIPE_CONNECT
+ /* Info about the EHLO response stored to / retrieved from cache. When
+ operating early-pipe, we use the cached values. For each of plaintext and
+ crypted we store bitmaps for ESMTP features and AUTH methods. If the LIMITS
+ extension is built and usable them at least one of the limits values cached
+ is nonzero, and we use the values to constrain the connection. */
+ ehlo_resp_precis ehlo_resp;
+#endif
+
+ struct timeval delivery_start;
+ address_item * first_addr;
+ address_item * next_addr;
+ address_item * sync_addr;
+
+ client_conn_ctx cctx;
+ smtp_inblock inblock;
+ smtp_outblock outblock;
+ uschar buffer[DELIVER_BUFFER_SIZE];
+ uschar inbuffer[4096];
+ uschar outbuffer[4096];
+} smtp_context;
+
+extern int smtp_setup_conn(smtp_context *, BOOL);
+extern int smtp_write_mail_and_rcpt_cmds(smtp_context *, int *);
+extern int smtp_reap_early_pipe(smtp_context *, int *);
+
+
+/* Data for reading the private options. */
+
+extern optionlist smtp_transport_options[];
+extern int smtp_transport_options_count;
+
+/* Block containing default values. */
+
+extern smtp_transport_options_block smtp_transport_option_defaults;
+
+/* The main, init, and closedown entry points for the transport */
+
+extern BOOL smtp_transport_entry(transport_instance *, address_item *);
+extern void smtp_transport_init(transport_instance *);
+extern void smtp_transport_closedown(transport_instance *);
+
+
+
+#ifdef SUPPORT_SOCKS
+extern int socks_sock_connect(host_item *, int, int, uschar *,
+ transport_instance *, int);
+#endif
+
+/* End of transports/smtp.h */
diff --git a/src/transports/smtp_socks.c b/src/transports/smtp_socks.c
new file mode 100644
index 0000000..0e58732
--- /dev/null
+++ b/src/transports/smtp_socks.c
@@ -0,0 +1,415 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2021 - 2022 */
+/* Copyright (c) Jeremy Harris 2015 - 2018 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+/* SOCKS version 5 proxy, client-mode */
+
+#include "../exim.h"
+#include "smtp.h"
+
+#ifdef SUPPORT_SOCKS /* entire file */
+
+#ifndef nelem
+# define nelem(arr) (sizeof(arr)/sizeof(*arr))
+#endif
+
+
+/* Defaults */
+#define SOCKS_PORT 1080
+#define SOCKS_TIMEOUT 5
+#define SOCKS_WEIGHT 1
+#define SOCKS_PRIORITY 1
+
+#define AUTH_NONE 0
+#define AUTH_NAME 2 /* user/password per RFC 1929 */
+#define AUTH_NAME_VER 1
+
+struct socks_err
+ {
+ uschar * reason;
+ int errcode;
+ } socks_errs[] =
+ {
+ {NULL, 0},
+ {US"general SOCKS server failure", EIO},
+ {US"connection not allowed by ruleset", EACCES},
+ {US"Network unreachable", ENETUNREACH},
+ {US"Host unreachable", EHOSTUNREACH},
+ {US"Connection refused", ECONNREFUSED},
+ {US"TTL expired", ECANCELED},
+ {US"Command not supported", EOPNOTSUPP},
+ {US"Address type not supported", EAFNOSUPPORT}
+ };
+
+typedef struct
+ {
+ const uschar * proxy_host;
+ uschar auth_type; /* RFC 1928 encoding */
+ const uschar * auth_name;
+ const uschar * auth_pwd;
+ short port;
+ BOOL is_failed;
+ unsigned timeout;
+ unsigned weight;
+ unsigned priority;
+ } socks_opts;
+
+static void
+socks_option_defaults(socks_opts * sob)
+{
+sob->proxy_host = NULL;
+sob->auth_type = AUTH_NONE;
+sob->auth_name = US"";
+sob->auth_pwd = US"";
+sob->is_failed = FALSE;
+sob->port = SOCKS_PORT;
+sob->timeout = SOCKS_TIMEOUT;
+sob->weight = SOCKS_WEIGHT;
+sob->priority = SOCKS_PRIORITY;
+}
+
+static void
+socks_option(socks_opts * sob, const uschar * opt)
+{
+if (Ustrncmp(opt, "auth=", 5) == 0)
+ {
+ opt += 5;
+ if (Ustrcmp(opt, "none") == 0) sob->auth_type = AUTH_NONE;
+ else if (Ustrcmp(opt, "name") == 0) sob->auth_type = AUTH_NAME;
+ }
+else if (Ustrncmp(opt, "name=", 5) == 0)
+ sob->auth_name = opt + 5;
+else if (Ustrncmp(opt, "pass=", 5) == 0)
+ sob->auth_pwd = opt + 5;
+else if (Ustrncmp(opt, "port=", 5) == 0)
+ sob->port = atoi(CCS opt + 5);
+else if (Ustrncmp(opt, "tmo=", 4) == 0)
+ sob->timeout = atoi(CCS opt + 4);
+else if (Ustrncmp(opt, "pri=", 4) == 0)
+ sob->priority = atoi(CCS opt + 4);
+else if (Ustrncmp(opt, "weight=", 7) == 0)
+ sob->weight = atoi(CCS opt + 7);
+return;
+}
+
+static int
+socks_auth(int fd, int method, socks_opts * sob, time_t tmo)
+{
+uschar * s;
+int len, i, j;
+
+switch(method)
+ {
+ default:
+ log_write(0, LOG_MAIN|LOG_PANIC,
+ "Unrecognised socks auth method %d", method);
+ return FAIL;
+ case AUTH_NONE:
+ return OK;
+ case AUTH_NAME:
+ HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" socks auth NAME '%s' '%s'\n",
+ sob->auth_name, sob->auth_pwd);
+ i = Ustrlen(sob->auth_name);
+ j = Ustrlen(sob->auth_pwd);
+ s = string_sprintf("%c%c%.255s%c%.255s", AUTH_NAME_VER,
+ i, sob->auth_name, j, sob->auth_pwd);
+ len = i + j + 3;
+ HDEBUG(D_transport|D_acl|D_v)
+ {
+ debug_printf_indent(" SOCKS>>");
+ for (int i = 0; i<len; i++) debug_printf(" %02x", s[i]);
+ debug_printf("\n");
+ }
+ if (send(fd, s, len, 0) < 0)
+ return FAIL;
+#ifdef TCP_QUICKACK
+ (void) setsockopt(fd, IPPROTO_TCP, TCP_QUICKACK, US &off, sizeof(off));
+#endif
+ if (!fd_ready(fd, tmo) || read(fd, s, 2) != 2)
+ return FAIL;
+ HDEBUG(D_transport|D_acl|D_v)
+ debug_printf_indent(" SOCKS<< %02x %02x\n", s[0], s[1]);
+ if (s[0] == AUTH_NAME_VER && s[1] == 0)
+ {
+ HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" socks auth OK\n");
+ return OK;
+ }
+
+ log_write(0, LOG_MAIN|LOG_PANIC, "socks auth failed");
+ errno = EPROTO;
+ return FAIL;
+ }
+}
+
+
+
+/* Find a suitable proxy to use from the list.
+Possible common code with spamd_get_server() ?
+
+Return: index into proxy spec array, or -1
+*/
+
+static int
+socks_get_proxy(socks_opts * proxies, unsigned nproxies)
+{
+unsigned int i;
+socks_opts * sd;
+socks_opts * lim = &proxies[nproxies];
+long rnd, weights;
+unsigned pri;
+
+if (nproxies == 1) /* shortcut, if we have only 1 server */
+ return (proxies[0].is_failed ? -1 : 0);
+
+/* scan for highest pri */
+for (pri = 0, sd = proxies; sd < lim; sd++)
+ if (!sd->is_failed && sd->priority > pri)
+ pri = sd->priority;
+
+/* get sum of weights at this pri */
+for (weights = 0, sd = proxies; sd < lim; sd++)
+ if (!sd->is_failed && sd->priority == pri)
+ weights += sd->weight;
+if (weights == 0) /* all servers failed */
+ return -1;
+
+for (rnd = random_number(weights), i = 0; i < nproxies; i++)
+ {
+ sd = &proxies[i];
+ if (!sd->is_failed && sd->priority == pri)
+ if ((rnd -= sd->weight) < 0)
+ return i;
+ }
+
+log_write(0, LOG_MAIN|LOG_PANIC,
+ "%s unknown error (memory/cpu corruption?)", __FUNCTION__);
+return -1;
+}
+
+
+
+/* Make a connection via a socks proxy
+
+Arguments:
+ host smtp target host
+ host_af address family
+ port remote tcp port number
+ interface local interface
+ tb transport
+ timeout connection timeout (zero for indefinite)
+
+Return value:
+ 0 on success; -1 on failure, with errno set
+*/
+
+int
+socks_sock_connect(host_item * host, int host_af, int port, uschar * interface,
+ transport_instance * tb, int timeout)
+{
+smtp_transport_options_block * ob =
+ (smtp_transport_options_block *)tb->options_block;
+const uschar * proxy_list;
+const uschar * proxy_spec;
+int sep = 0;
+int fd;
+time_t tmo;
+const uschar * state;
+uschar buf[24];
+socks_opts proxies[32]; /* max #proxies handled */
+unsigned nproxies;
+socks_opts * sob = NULL;
+unsigned size;
+blob early_data;
+
+if (!timeout) timeout = 24*60*60; /* use 1 day for "indefinite" */
+tmo = time(NULL) + timeout;
+
+if (!(proxy_list = expand_string(ob->socks_proxy)))
+ {
+ log_write(0, LOG_MAIN|LOG_PANIC, "Bad expansion for socks_proxy in %s",
+ tb->name);
+ return -1;
+ }
+
+/* Read proxy list */
+
+for (nproxies = 0;
+ nproxies < nelem(proxies)
+ && (proxy_spec = string_nextinlist(&proxy_list, &sep, NULL, 0));
+ nproxies++)
+ {
+ int subsep = -' ';
+ const uschar * option;
+
+ socks_option_defaults(sob = &proxies[nproxies]);
+
+ if (!(sob->proxy_host = string_nextinlist(&proxy_spec, &subsep, NULL, 0)))
+ {
+ /* paniclog config error */
+ return -1;
+ }
+
+ /*XXX consider global options eg. "hide socks_password = wibble" on the tpt */
+ /* extract any further per-proxy options */
+ while ((option = string_nextinlist(&proxy_spec, &subsep, NULL, 0)))
+ socks_option(sob, option);
+ }
+if (!sob) return -1;
+
+/* Set up the socks protocol method-selection message,
+for sending on connection */
+
+state = US"method select";
+buf[0] = 5; buf[1] = 1; buf[2] = sob->auth_type;
+early_data.data = buf;
+early_data.len = 3;
+
+/* Try proxies until a connection succeeds */
+
+for(;;)
+ {
+ int idx;
+ host_item proxy;
+ smtp_connect_args sc = {.sock = -1};
+
+ if ((idx = socks_get_proxy(proxies, nproxies)) < 0)
+ {
+ HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" no proxies left\n");
+ errno = EBUSY;
+ return -1;
+ }
+ sob = &proxies[idx];
+
+ /* bodge up a host struct for the proxy */
+ proxy.address = proxy.name = sob->proxy_host;
+ proxy.port = sob->port;
+
+ sc.tblock = tb;
+ sc.ob = ob;
+ sc.host = &proxy;
+ sc.host_af = Ustrchr(sob->proxy_host, ':') ? AF_INET6 : AF_INET;
+ sc.interface = interface;
+
+ /*XXX we trust that the method-select command is idempotent */
+ if ((fd = smtp_sock_connect(&sc, sob->timeout, &early_data)) >= 0)
+ {
+ proxy_local_address = string_copy(proxy.address);
+ proxy_local_port = sob->port;
+ break;
+ }
+
+ log_write(0, LOG_MAIN, "%s: %s", __FUNCTION__, strerror(errno));
+ sob->is_failed = TRUE;
+ }
+
+/* Do the socks protocol stuff */
+
+HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" SOCKS>> 05 01 %02x\n", sob->auth_type);
+
+/* expect method response */
+
+#ifdef TCP_QUICKACK
+(void) setsockopt(fd, IPPROTO_TCP, TCP_QUICKACK, US &off, sizeof(off));
+#endif
+
+if ( !fd_ready(fd, tmo)
+ || read(fd, buf, 2) != 2
+ )
+ goto rcv_err;
+HDEBUG(D_transport|D_acl|D_v)
+ debug_printf_indent(" SOCKS<< %02x %02x\n", buf[0], buf[1]);
+if ( buf[0] != 5
+ || socks_auth(fd, buf[1], sob, tmo) != OK
+ )
+ goto proxy_err;
+
+ {
+ union sockaddr_46 sin;
+ (void) ip_addr(&sin, host_af, host->address, port);
+
+ /* send connect (ipver, ipaddr, port) */
+
+ buf[0] = 5; buf[1] = 1; buf[2] = 0; buf[3] = host_af == AF_INET6 ? 4 : 1;
+ #if HAVE_IPV6
+ if (host_af == AF_INET6)
+ {
+ memcpy(buf+4, &sin.v6.sin6_addr, sizeof(sin.v6.sin6_addr));
+ memcpy(buf+4+sizeof(sin.v6.sin6_addr),
+ &sin.v6.sin6_port, sizeof(sin.v6.sin6_port));
+ size = 4+sizeof(sin.v6.sin6_addr)+sizeof(sin.v6.sin6_port);
+ }
+ else
+ #endif
+ {
+ memcpy(buf+4, &sin.v4.sin_addr.s_addr, sizeof(sin.v4.sin_addr.s_addr));
+ memcpy(buf+4+sizeof(sin.v4.sin_addr.s_addr),
+ &sin.v4.sin_port, sizeof(sin.v4.sin_port));
+ size = 4+sizeof(sin.v4.sin_addr.s_addr)+sizeof(sin.v4.sin_port);
+ }
+ }
+
+state = US"connect";
+HDEBUG(D_transport|D_acl|D_v)
+ {
+ debug_printf_indent(" SOCKS>>");
+ for (int i = 0; i<size; i++) debug_printf(" %02x", buf[i]);
+ debug_printf("\n");
+ }
+if (send(fd, buf, size, 0) < 0)
+ goto snd_err;
+
+/* expect conn-reply (success, local(ipver, addr, port))
+of same length as conn-request, or non-success fail code */
+
+if ( !fd_ready(fd, tmo)
+ || (size = read(fd, buf, size)) < 2
+ )
+ goto rcv_err;
+HDEBUG(D_transport|D_acl|D_v)
+ {
+ debug_printf_indent(" SOCKS>>");
+ for (int i = 0; i<size; i++) debug_printf(" %02x", buf[i]);
+ debug_printf("\n");
+ }
+if ( buf[0] != 5
+ || buf[1] != 0
+ )
+ goto proxy_err;
+
+proxy_external_address = string_copy(
+ host_ntoa(buf[3] == 4 ? AF_INET6 : AF_INET, buf+4, NULL, NULL));
+proxy_external_port = ntohs(*((uint16_t *)(buf + (buf[3] == 4 ? 20 : 8))));
+proxy_session = TRUE;
+
+HDEBUG(D_transport|D_acl|D_v)
+ debug_printf_indent(" proxy farside: [%s]:%d\n", proxy_external_address, proxy_external_port);
+
+return fd;
+
+snd_err:
+ HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" proxy snd_err %s: %s\n", state, strerror(errno));
+ return -1;
+
+proxy_err:
+ {
+ struct socks_err * se =
+ buf[1] > nelem(socks_errs) ? NULL : socks_errs + buf[1];
+ HDEBUG(D_transport|D_acl|D_v)
+ debug_printf_indent(" proxy %s: %s\n", state, se ? se->reason : US"unknown error code received");
+ errno = se ? se->errcode : EPROTO;
+ }
+
+rcv_err:
+ HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" proxy rcv_err %s: %s\n", state, strerror(errno));
+ if (!errno) errno = EPROTO;
+ else if (errno == ENOENT) errno = ECONNABORTED;
+ return -1;
+}
+
+#endif /* entire file */
+/* vi: aw ai sw=2
+*/
diff --git a/src/transports/tf_maildir.c b/src/transports/tf_maildir.c
new file mode 100644
index 0000000..a83fc6f
--- /dev/null
+++ b/src/transports/tf_maildir.c
@@ -0,0 +1,585 @@
+/*************************************************
+* 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. */
+
+/* Functions in support of the use of maildirsize files for handling quotas in
+maildir directories. Some of the rules are a bit baroque:
+
+http://www.inter7.com/courierimap/README.maildirquota.html
+
+We try to follow most of that, except that the directories to skip for quota
+calculations are not hard wired in, but are supplied as a regex. */
+
+
+#include "../exim.h"
+#include "appendfile.h"
+#include "tf_maildir.h"
+
+#define MAX_FILE_SIZE 5120
+
+
+
+/*************************************************
+* Ensure maildir directories exist *
+*************************************************/
+
+/* This function is called at the start of a maildir delivery, to ensure that
+all the relevant directories exist. It also creates a maildirfolder file if the
+base directory matches a given pattern.
+
+Argument:
+ path the base directory name
+ addr the address item (for setting an error message)
+ create_directory true if we are allowed to create missing directories
+ dirmode the mode for created directories
+ maildirfolder_create_regex
+ the pattern to match for maildirfolder creation
+
+Returns: TRUE on success; FALSE on failure
+*/
+
+BOOL maildir_ensure_directories(uschar *path, address_item *addr,
+ BOOL create_directory, int dirmode, uschar *maildirfolder_create_regex)
+{
+int i;
+struct stat statbuf;
+const char *subdirs[] = { "/tmp", "/new", "/cur" };
+
+DEBUG(D_transport)
+ debug_printf("ensuring maildir directories exist in %s\n", path);
+
+/* First ensure that the path we have is a directory; if it does not exist,
+create it. Then make sure the tmp, new & cur subdirs of the maildir are
+there. If not, fail. This aborts the delivery (even though the cur subdir is
+not actually needed for delivery). Handle all 4 directory tests/creates in a
+loop so that code can be shared. */
+
+for (i = 0; i < 4; i++)
+ {
+ int j;
+ const uschar *dir, *mdir;
+
+ if (i == 0)
+ {
+ mdir = CUS"";
+ dir = path;
+ }
+ else
+ {
+ mdir = CUS subdirs[i-1];
+ dir = mdir + 1;
+ }
+
+ /* Check an existing path is a directory. This is inside a loop because
+ there is a potential race condition when creating the directory - some
+ other process may get there first. Give up after trying several times,
+ though. */
+
+ for (j = 0; j < 10; j++)
+ {
+ if (Ustat(dir, &statbuf) == 0)
+ {
+ if (S_ISDIR(statbuf.st_mode)) break; /* out of the race loop */
+ addr->message = string_sprintf("%s%s is not a directory", path,
+ mdir);
+ addr->basic_errno = ERRNO_NOTDIRECTORY;
+ return FALSE;
+ }
+
+ /* Try to make if non-existent and configured to do so */
+
+ if (errno == ENOENT && create_directory)
+ {
+ if (!directory_make(NULL, dir, dirmode, FALSE))
+ {
+ if (errno == EEXIST) continue; /* repeat the race loop */
+ addr->message = string_sprintf("cannot create %s%s", path, mdir);
+ addr->basic_errno = errno;
+ return FALSE;
+ }
+ DEBUG(D_transport)
+ debug_printf("created directory %s%s\n", path, mdir);
+ break; /* out of the race loop */
+ }
+
+ /* stat() error other than ENOENT, or ENOENT and not creatable */
+
+ addr->message = string_sprintf("stat() error for %s%s: %s", path, mdir,
+ strerror(errno));
+ addr->basic_errno = errno;
+ return FALSE;
+ }
+
+ /* If we went round the loop 10 times, the directory was flickering in
+ and out of existence like someone in a malfunctioning Star Trek
+ transporter. */
+
+ if (j >= 10)
+ {
+ addr->message = string_sprintf("existence of %s%s unclear\n", path,
+ mdir);
+ addr->basic_errno = errno;
+ addr->special_action = SPECIAL_FREEZE;
+ return FALSE;
+ }
+
+ /* First time through the directories loop, cd to the main directory */
+
+ if (i == 0 && Uchdir(path) != 0)
+ {
+ addr->message = string_sprintf ("cannot chdir to %s", path);
+ addr->basic_errno = errno;
+ return FALSE;
+ }
+ }
+
+/* If the basic path matches maildirfolder_create_regex, we are dealing with
+a subfolder, and should ensure that a maildirfolder file exists. */
+
+if (maildirfolder_create_regex)
+ {
+ int err;
+ PCRE2_SIZE offset;
+ const pcre2_code * re;
+
+ DEBUG(D_transport) debug_printf("checking for maildirfolder requirement\n");
+
+ if (!(re = pcre2_compile((PCRE2_SPTR)maildirfolder_create_regex,
+ PCRE2_ZERO_TERMINATED, PCRE_COPT, &err, &offset, pcre_cmp_ctx)))
+ {
+ uschar errbuf[128];
+ pcre2_get_error_message(err, errbuf, sizeof(errbuf));
+ addr->message = string_sprintf("appendfile: regular expression "
+ "error: %s at offset %ld while compiling %s", errbuf, (long)offset,
+ maildirfolder_create_regex);
+ return FALSE;
+ }
+
+ if (regex_match(re, path, -1, NULL))
+ {
+ uschar *fname = string_sprintf("%s/maildirfolder", path);
+ if (Ustat(fname, &statbuf) == 0)
+ {
+ DEBUG(D_transport) debug_printf("maildirfolder already exists\n");
+ }
+ else
+ {
+ int fd = Uopen(fname, O_WRONLY|O_APPEND|O_CREAT, 0600);
+ if (fd < 0)
+ {
+ addr->message = string_sprintf("appendfile: failed to create "
+ "maildirfolder file in %s directory: %s", path, strerror(errno));
+ return FALSE;
+ }
+ (void)close(fd);
+ DEBUG(D_transport) debug_printf("created maildirfolder file\n");
+ }
+ }
+ else
+ {
+ DEBUG(D_transport) debug_printf("maildirfolder file not required\n");
+ }
+ }
+
+return TRUE; /* Everything exists that should exist */
+}
+
+
+
+
+/*************************************************
+* Update maildirsizefile for new file *
+*************************************************/
+
+/* This function is called to add a new line to the file, recording the length
+of the newly added message. There isn't much we can do on failure...
+
+Arguments:
+ fd the open file descriptor
+ size the size of the message
+
+Returns: nothing
+*/
+
+void
+maildir_record_length(int fd, int size)
+{
+int len;
+uschar buffer[256];
+sprintf(CS buffer, "%d 1\n", size);
+len = Ustrlen(buffer);
+if (lseek(fd, 0, SEEK_END) >= 0)
+ {
+ len = write(fd, buffer, len);
+ DEBUG(D_transport)
+ debug_printf("added '%.*s' to maildirsize file\n", len-1, buffer);
+ }
+}
+
+
+
+/*************************************************
+* Find the size of a maildir *
+*************************************************/
+
+/* This function is called when we have to recalculate the size of a maildir by
+scanning all the files and directories therein. There are rules and conventions
+about which files or directories are included. We support this by the use of a
+regex to match directories that are to be included.
+
+Maildirs can only be one level deep. However, this function recurses, so it
+might cope with deeper nestings. We use the existing check_dir_size() function
+to add up the sizes of the files in a directory that contains messages.
+
+The function returns the most recent timestamp encountered. It can also be run
+in a dummy mode in which it does not scan for sizes, but just returns the
+timestamp.
+
+Arguments:
+ path the path to the maildir
+ filecount where to store the count of messages
+ latest where to store the latest timestamp encountered
+ regex a regex for getting files sizes from file names
+ dir_regex a regex for matching directories to be included
+ timestamp_only don't actually compute any sizes
+
+Returns: the sum of the sizes of the messages
+*/
+
+off_t
+maildir_compute_size(uschar *path, int *filecount, time_t *latest,
+ const pcre2_code *regex, const pcre2_code *dir_regex, BOOL timestamp_only)
+{
+DIR *dir;
+off_t sum = 0;
+
+if (!(dir = exim_opendir(path)))
+ return 0;
+
+for (struct dirent *ent; ent = readdir(dir); )
+ {
+ uschar * s, * name = US ent->d_name;
+ struct stat statbuf;
+
+ if (Ustrcmp(name, ".") == 0 || Ustrcmp(name, "..") == 0) continue;
+
+ /* We are normally supplied with a regex for choosing which directories to
+ scan. We do the regex match first, because that avoids a stat() for names
+ we aren't interested in. */
+
+ if (dir_regex && !regex_match(dir_regex, name, -1, NULL))
+ {
+ DEBUG(D_transport)
+ debug_printf("skipping %s/%s: dir_regex does not match\n", path, name);
+ continue;
+ }
+
+ /* The name is OK; stat it. */
+
+ s = string_sprintf("%s/%s", path, name);
+ if (Ustat(s, &statbuf) < 0)
+ {
+ DEBUG(D_transport)
+ debug_printf("maildir_compute_size: stat error %d for %s: %s\n", errno,
+ s, strerror(errno));
+ continue;
+ }
+
+ if ((statbuf.st_mode & S_IFMT) != S_IFDIR)
+ {
+ DEBUG(D_transport)
+ debug_printf("skipping %s/%s: not a directory\n", s, name);
+ continue;
+ }
+
+ /* Keep the latest timestamp encountered */
+
+ if (statbuf.st_mtime > *latest) *latest = statbuf.st_mtime;
+
+ /* If this is a maildir folder, call this function recursively. */
+
+ if (name[0] == '.')
+ sum += maildir_compute_size(s, filecount, latest, regex, dir_regex,
+ timestamp_only);
+
+ /* Otherwise it must be a folder that contains messages (e.g. new or cur), so
+ we need to get its size, unless all we are interested in is the timestamp. */
+
+ else if (!timestamp_only)
+ sum += check_dir_size(s, filecount, regex);
+ }
+
+closedir(dir);
+DEBUG(D_transport)
+ {
+ if (timestamp_only)
+ debug_printf("maildir_compute_size (timestamp_only): %ld\n",
+ (long int) *latest);
+ else
+ debug_printf("maildir_compute_size: path=%s\n sum=" OFF_T_FMT
+ " filecount=%d timestamp=%ld\n",
+ path, sum, *filecount, (long int) *latest);
+ }
+return sum;
+}
+
+
+
+/*************************************************
+* Create or update maildirsizefile *
+*************************************************/
+
+/* This function is called before a delivery if the option to use
+maildirsizefile is enabled. Its function is to create the file if it does not
+exist, or to update it if that is necessary.
+
+The logic in this function follows the rules that are described in
+
+ http://www.inter7.com/courierimap/README.maildirquota.html
+
+Or, at least, it is supposed to!
+
+Arguments:
+ path the path to the maildir directory; this is already backed-up
+ to the parent if the delivery directory is a maildirfolder
+ ob the appendfile options block
+ regex a compiled regex for getting a file's size from its name
+ dir_regex a compiled regex for selecting maildir directories
+ returned_size where to return the current size of the maildir, even if
+ the maildirsizefile is removed because of a race
+
+Returns: >=0 a file descriptor for an open maildirsize file
+ -1 there was an error opening or accessing the file
+ -2 the file was removed because of a race
+*/
+
+int
+maildir_ensure_sizefile(uschar *path, appendfile_transport_options_block *ob,
+ const pcre2_code *regex, const pcre2_code *dir_regex, off_t *returned_size,
+ int *returned_filecount)
+{
+int count, fd;
+off_t cached_quota = 0;
+int cached_quota_filecount = 0;
+int filecount = 0;
+int linecount = 0;
+off_t size = 0;
+uschar *filename;
+uschar buffer[MAX_FILE_SIZE];
+uschar *ptr = buffer;
+uschar *endptr;
+
+/* Try a few times to open or create the file, in case another process is doing
+the same thing. */
+
+filename = string_sprintf("%s/maildirsize", path);
+
+DEBUG(D_transport) debug_printf("looking for maildirsize in %s\n", path);
+if ((fd = Uopen(filename, O_RDWR|O_APPEND, ob->mode ? ob->mode : 0600)) < 0)
+ {
+ if (errno != ENOENT) return -1;
+ DEBUG(D_transport)
+ debug_printf("%s does not exist: recalculating\n", filename);
+ goto RECALCULATE;
+ }
+
+/* The file has been successfully opened. Check that the cached quota value is
+still correct, and that the size of the file is still small enough. If so,
+compute the maildir size from the file. */
+
+if ((count = read(fd, buffer, sizeof(buffer))) >= sizeof(buffer))
+ {
+ DEBUG(D_transport)
+ debug_printf("maildirsize file too big (%d): recalculating\n", count);
+ goto RECALCULATE;
+ }
+buffer[count] = 0; /* Ensure string terminated */
+
+/* Read the quota parameters from the first line of the data. */
+
+DEBUG(D_transport)
+ debug_printf("reading quota parameters from maildirsize data\n");
+
+for (;;)
+ {
+ off_t n = (off_t)Ustrtod(ptr, &endptr);
+
+ /* Only two data items are currently defined; ignore any others that
+ may be present. The spec is for a number followed by a letter. Anything
+ else we reject and recalculate. */
+
+ if (*endptr == 'S') cached_quota = n;
+ else if (*endptr == 'C') cached_quota_filecount = (int)n;
+ if (!isalpha(*endptr++))
+ {
+ DEBUG(D_transport)
+ debug_printf("quota parameter number not followed by letter in "
+ "\"%.*s\": recalculating maildirsize\n", (int)(endptr - buffer),
+ buffer);
+ goto RECALCULATE;
+ }
+ if (*endptr == '\n' || *endptr == 0) break;
+ if (*endptr++ != ',')
+ {
+ DEBUG(D_transport)
+ debug_printf("quota parameter not followed by comma in "
+ "\"%.*s\": recalculating maildirsize\n", (int)(endptr - buffer),
+ buffer);
+ goto RECALCULATE;
+ }
+ ptr = endptr;
+ }
+
+/* Check the cached values against the current settings */
+
+if (cached_quota != ob->quota_value ||
+ cached_quota_filecount != ob->quota_filecount_value)
+ {
+ DEBUG(D_transport)
+ debug_printf("cached quota is out of date: recalculating\n"
+ " quota=" OFF_T_FMT " cached_quota=" OFF_T_FMT " filecount_quota=%d "
+ "cached_quota_filecount=%d\n", ob->quota_value,
+ cached_quota, ob->quota_filecount_value, cached_quota_filecount);
+ goto RECALCULATE;
+ }
+
+/* Quota values agree; parse the rest of the data to get the sizes. At this
+stage, *endptr points either to 0 or to '\n'. */
+
+DEBUG(D_transport)
+ debug_printf("computing maildir size from maildirsize data\n");
+
+while (*endptr++ == '\n')
+ {
+ if (*endptr == 0) break;
+ linecount++;
+ ptr = endptr;
+ size += (off_t)Ustrtod(ptr, &endptr);
+ if (*endptr != ' ') break;
+ ptr = endptr + 1;
+ filecount += Ustrtol(ptr, &endptr, 10);
+ }
+
+/* If *endptr is zero, we have successfully parsed the file, and we now have
+the size of the mailbox as cached in the file. The "rules" say that if this
+value indicates that the mailbox is over quota, we must recalculate if there is
+more than one entry in the file, or if the file is older than 15 minutes. Also,
+just in case there are weird values in the file, recalculate if either of the
+values is negative. */
+
+if (*endptr == 0)
+ {
+ if (size < 0 || filecount < 0)
+ {
+ DEBUG(D_transport) debug_printf("negative value in maildirsize "
+ "(size=" OFF_T_FMT " count=%d): recalculating\n", size, filecount);
+ goto RECALCULATE;
+ }
+
+ if (ob->quota_value > 0 &&
+ (size + (ob->quota_is_inclusive? message_size : 0) > ob->quota_value ||
+ (ob->quota_filecount_value > 0 &&
+ filecount + (ob->quota_is_inclusive ? 1:0) >
+ ob->quota_filecount_value)
+ ))
+ {
+ struct stat statbuf;
+ if (linecount > 1)
+ {
+ DEBUG(D_transport) debug_printf("over quota and maildirsize has "
+ "more than 1 entry: recalculating\n");
+ goto RECALCULATE;
+ }
+
+ if (fstat(fd, &statbuf) < 0) goto RECALCULATE; /* Should never occur */
+
+ if (time(NULL) - statbuf.st_mtime > 15*60)
+ {
+ DEBUG(D_transport) debug_printf("over quota and maildirsize is older "
+ "than 15 minutes: recalculating\n");
+ goto RECALCULATE;
+ }
+ }
+ }
+
+
+/* If *endptr is not zero, there was a syntax error in the file. */
+
+else
+ {
+ int len;
+ time_t old_latest, new_latest;
+ uschar *tempname;
+ struct timeval tv;
+
+ DEBUG(D_transport)
+ {
+ uschar *p = endptr;
+ while (p > buffer && p[-1] != '\n') p--;
+ endptr[1] = 0;
+
+ debug_printf("error in maildirsizefile: unexpected character %d in "
+ "line %d (starting '%s'): recalculating\n",
+ *endptr, linecount + 1, string_printing(p));
+ }
+
+ /* Either there is no file, or the quota value has changed, or the file has
+ got too big, or there was some format error in the file. Recalculate the size
+ and write new contents to a temporary file; then rename it. After any
+ error, just return -1 as the file descriptor. */
+
+ RECALCULATE:
+
+ if (fd >= 0) (void)close(fd);
+ old_latest = 0;
+ filecount = 0;
+ size = maildir_compute_size(path, &filecount, &old_latest, regex, dir_regex,
+ FALSE);
+
+ (void)gettimeofday(&tv, NULL);
+ tempname = string_sprintf("%s/tmp/" TIME_T_FMT ".H%luP%lu.%s",
+ path, tv.tv_sec, tv.tv_usec, (long unsigned) getpid(), primary_hostname);
+
+ fd = Uopen(tempname, O_RDWR|O_CREAT|O_EXCL, ob->mode ? ob->mode : 0600);
+ if (fd >= 0)
+ {
+ (void)sprintf(CS buffer, OFF_T_FMT "S,%dC\n" OFF_T_FMT " %d\n",
+ ob->quota_value, ob->quota_filecount_value, size, filecount);
+ len = Ustrlen(buffer);
+ if (write(fd, buffer, len) != len || Urename(tempname, filename) < 0)
+ {
+ (void)close(fd);
+ fd = -1;
+ }
+ }
+
+ /* If any of the directories have been modified since the last timestamp we
+ saw, we have to junk this maildirsize file. */
+
+ DEBUG(D_transport) debug_printf("checking subdirectory timestamps\n");
+ new_latest = 0;
+ (void)maildir_compute_size(path, NULL, &new_latest , NULL, dir_regex, TRUE);
+ if (new_latest > old_latest)
+ {
+ DEBUG(D_transport) debug_printf("abandoning maildirsize because of "
+ "a later subdirectory modification\n");
+ (void)Uunlink(filename);
+ (void)close(fd);
+ fd = -2;
+ }
+ }
+
+/* Return the sizes and the file descriptor, if any */
+
+DEBUG(D_transport) debug_printf("returning maildir size=" OFF_T_FMT
+ " filecount=%d\n", size, filecount);
+*returned_size = size;
+*returned_filecount = filecount;
+return fd;
+}
+
+/* End of tf_maildir.c */
diff --git a/src/transports/tf_maildir.h b/src/transports/tf_maildir.h
new file mode 100644
index 0000000..b3707b1
--- /dev/null
+++ b/src/transports/tf_maildir.h
@@ -0,0 +1,21 @@
+/*************************************************
+* 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. */
+
+/* Header file for the functions that are used to support the use of
+maildirsize files for quota handling in maildir directories. */
+
+extern off_t maildir_compute_size(uschar *, int *, time_t *, const pcre2_code *,
+ const pcre2_code *, BOOL);
+extern BOOL maildir_ensure_directories(uschar *, address_item *, BOOL, int,
+ uschar *);
+extern int maildir_ensure_sizefile(uschar *,
+ appendfile_transport_options_block *, const pcre2_code *,
+ const pcre2_code *, off_t *, int *);
+extern void maildir_record_length(int, int);
+
+/* End of tf_maildir.h */