diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 09:44:07 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 09:44:07 +0000 |
commit | 39ce00b8d520cbecbd6af87257e8fb11df0ec273 (patch) | |
tree | 4c21a2674c19e5c44be3b3550b476b9e63d8ae3d /src/transports | |
parent | Initial commit. (diff) | |
download | exim4-60bddf70a0555b5f386aa68ac440742aefbfa361.tar.xz exim4-60bddf70a0555b5f386aa68ac440742aefbfa361.zip |
Adding upstream version 4.94.2.upstream/4.94.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/transports')
-rw-r--r-- | src/transports/Makefile | 27 | ||||
-rw-r--r-- | src/transports/README | 41 | ||||
-rw-r--r-- | src/transports/appendfile.c | 3318 | ||||
-rw-r--r-- | src/transports/appendfile.h | 99 | ||||
-rw-r--r-- | src/transports/autoreply.c | 825 | ||||
-rw-r--r-- | src/transports/autoreply.h | 45 | ||||
-rw-r--r-- | src/transports/lmtp.c | 809 | ||||
-rw-r--r-- | src/transports/lmtp.h | 32 | ||||
-rw-r--r-- | src/transports/pipe.c | 1145 | ||||
-rw-r--r-- | src/transports/pipe.h | 51 | ||||
-rw-r--r-- | src/transports/queuefile.c | 282 | ||||
-rw-r--r-- | src/transports/queuefile.h | 29 | ||||
-rw-r--r-- | src/transports/smtp.c | 5640 | ||||
-rw-r--r-- | src/transports/smtp.h | 209 | ||||
-rw-r--r-- | src/transports/smtp_socks.c | 418 | ||||
-rw-r--r-- | src/transports/tf_maildir.c | 584 | ||||
-rw-r--r-- | src/transports/tf_maildir.h | 20 |
17 files changed, 13574 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..8ab8b60 --- /dev/null +++ b/src/transports/appendfile.c @@ -0,0 +1,3318 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* Copyright (c) The Exim maintainers 2020 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +#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 = { + NULL, /* filename */ + NULL, /* dirname */ + US"q${base62:$tod_epoch}-$inode", /* dirfilename */ + NULL, /* message_prefix (default reset in init if not bsmtp) */ + NULL, /* message_suffix (ditto) */ + US"anywhere", /* create_file_string (string value for create_file) */ + NULL, /* quota */ + NULL, /* quota_directory */ + NULL, /* quota_filecount */ + NULL, /* quota_size_regex */ + NULL, /* quota_warn_threshold */ + NULL, /* mailbox_size_string */ + NULL, /* mailbox_filecount_string */ + NULL, /* expand_maildir_use_size_file */ + US"^(?:cur|new|\\..*)$", /* maildir_dir_regex */ + NULL, /* maildir_tag */ + NULL, /* maildirfolder_create_regex */ + NULL, /* mailstore_prefix */ + NULL, /* mailstore_suffix */ + NULL, /* check_string (default changed for non-bsmtp file)*/ + NULL, /* escape_string (ditto) */ + NULL, /* file_format */ + 0, /* quota_value */ + 0, /* quota_warn_threshold_value */ + -1, /* mailbox_size_value */ + -1, /* mailbox_filecount_value */ + 0, /* quota_filecount_value */ + APPENDFILE_MODE, /* mode */ + APPENDFILE_DIRECTORY_MODE, /* dirmode */ + APPENDFILE_LOCKFILE_MODE, /* lockfile_mode */ + 30*60, /* lockfile_timeout */ + 0, /* lock_fcntl_timeout */ + 0, /* lock_flock_timeout */ + 10, /* lock_retries */ + 3, /* lock_interval */ + 10, /* maildir_retries */ + create_anywhere,/* create_file */ + 0, /* options */ + FALSE, /* allow_fifo */ + FALSE, /* allow_symlink */ + FALSE, /* check_group */ + TRUE, /* check_owner */ + TRUE, /* create_directory */ + FALSE, /* notify_comsat */ + TRUE, /* use_lockfile */ + FALSE, /* set_use_lockfile */ + TRUE, /* use_fcntl */ + FALSE, /* set_use_fcntl */ + FALSE, /* use_flock */ + FALSE, /* set_use_flock */ + FALSE, /* use_mbx_lock */ + FALSE, /* set_use_mbx_lock */ + FALSE, /* use_bsmtp */ + FALSE, /* use_crlf */ + FALSE, /* file_must_exist */ + TRUE, /* mode_fail_narrower */ + FALSE, /* maildir_format */ + FALSE, /* maildir_use_size_file */ + FALSE, /* mailstore_format */ + FALSE, /* mbx_format */ + FALSE, /* quota_warn_threshold_is_percent */ + TRUE, /* quota_is_inclusive */ + FALSE, /* quota_no_check */ + FALSE /* quota_filecount_no_check */ +}; + + +/* 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; + +addrlist = addrlist; /* Keep picky compilers happy */ +dummy = dummy; +uid = uid; +gid = gid; + +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; + int no_check = 0; + uschar *which = NULL; + + if (q == NULL) d = default_value; + else + { + uschar *rest; + uschar *s = expand_string(q); + + if (!s) + { + *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); + +/* 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 (ob->create_file_string) + { + int value = 0; + if (Ustrcmp(ob->create_file_string, "anywhere") == 0) + value = create_anywhere; + else if (Ustrcmp(ob->create_file_string, "belowhome") == 0) + value = create_belowhome; + else if (Ustrcmp(ob->create_file_string, "inhome") == 0) + value = create_inhome; + else + log_write(0, LOG_PANIC_DIE|LOG_CONFIG, + "invalid value given for \"file_create\" for the %s transport: %s", + tblock->name, ob->create_file_string); + ob->create_file = value; + } + +/* 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 */ + +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) + regex 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 pcre *regex) +{ +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 (regex) + { + int ovector[6]; + if (pcre_exec(regex, NULL, CS name, Ustrlen(name), 0, 0, ovector,6) >= 2) + { + uschar *endptr; + off_t size = (off_t)Ustrtod(name + ovector[2], &endptr); + if (endptr == name + ovector[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, regex); + } + +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 + +Returns: TRUE if creation is permitted +*/ + +static BOOL +check_creation(uschar *filename, int create_file) +{ +BOOL yield = TRUE; + +if (deliver_home && create_file != create_anywhere) + { + int len = Ustrlen(deliver_home); + uschar *file = filename; + + while (file[0] == '/' && file[1] == '/') file++; + if (Ustrncmp(file, deliver_home, 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]; + uschar *rph = deliver_home; + int rlen = Ustrlen(big_buffer); + + if ((rp = US realpath(CS deliver_home, 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_home); + } + } + } + #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; +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 (is_tainted(path)) + { + addr->message = string_sprintf("Tainted '%s' (file or directory " + "name for %s transport) not permitted", path, tblock->name); + 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; + } + +/* 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. */ + + dataname = filename = path; + allow_creation_here = check_creation(filename, ob->create_file); + + /* 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 = '\0'; + if (!directory_make(NULL, path, ob->dirmode, FALSE)) + { + addr->basic_errno = errno; + addr->message = + string_sprintf("failed to create directories for %s: %s", path, + strerror(errno)); + DEBUG(D_transport) debug_printf("%s transport: %s\n", tblock->name, path); + return FALSE; + } + *p = '/'; + } + + /* 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 = path; /* Default quota check path */ + const pcre *regex = NULL; /* Regex for file size from file name */ + + if (!check_creation(string_sprintf("%s/any", path), ob->create_file)) + { + 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; + } + + #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) + { + const uschar *error; + int offset; + + /* Compile the regex if there is one. */ + + if (ob->quota_size_regex) + { + if (!(regex = pcre_compile(CS ob->quota_size_regex, PCRE_COPT, + CCSS &error, &offset, NULL))) + { + addr->message = string_sprintf("appendfile: regular expression " + "error: %s at offset %d while compiling %s", error, 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 != NULL) + { + if (slash[1] == 0) + { + *slash = 0; + slash = Ustrrchr(new_check_path, '/'); + } + if (slash != NULL) + { + *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 pcre *dir_regex = NULL; + const uschar *error; + int offset; + + if (ob->maildir_dir_regex) + { + int check_path_len = Ustrlen(check_path); + + if (!(dir_regex = pcre_compile(CS ob->maildir_dir_regex, PCRE_COPT, + CCSS &error, &offset, NULL))) + { + addr->message = string_sprintf("appendfile: regular expression " + "error: %s at offset %d while compiling %s", error, 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 == 0) ? US "new" : string_sprintf("%s/new", s); + if (pcre_exec(dir_regex, NULL, CS s, Ustrlen(s), 0, 0, NULL, 0) < 0) + { + 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, regex, 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, + * regex, 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, regex); + 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 ".H%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 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, is_tainted(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, ×); + +/* 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; + +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..4f0f126 --- /dev/null +++ b/src/transports/appendfile.h @@ -0,0 +1,99 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* 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 pcre *); + +/* End of transports/appendfile.h */ diff --git a/src/transports/autoreply.c b/src/transports/autoreply.c new file mode 100644 index 0000000..865abbf --- /dev/null +++ b/src/transports/autoreply.c @@ -0,0 +1,825 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* Copyright (c) The Exim Maintainers 2020 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +#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. */ + +autoreply_transport_options_block autoreply_transport_option_defaults = { + NULL, /* from */ + NULL, /* reply_to */ + NULL, /* to */ + NULL, /* cc */ + NULL, /* bcc */ + NULL, /* subject */ + NULL, /* headers */ + NULL, /* text */ + NULL, /* file */ + NULL, /* logfile */ + NULL, /* oncelog */ + NULL, /* once_repeat */ + NULL, /* never_mail */ + 0600, /* mode */ + 0, /* once_file_size */ + FALSE, /* file_expand */ + FALSE, /* file_optional */ + FALSE /* return message */ +}; + + + +/* 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: + listptr points to the list of addresses + never_mail an address list, already expanded + +Returns: nothing +*/ + +static void +check_never_mail(uschar **listptr, const uschar *never_mail) +{ +uschar *s = *listptr; + +while (*s != 0) + { + 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); + if (terminator == ',') e++; + memmove(s, e, Ustrlen(e) + 1); + } + else /* Skip over this address */ + { + s = e; + if (terminator == ',') s++; + } + } + +/* Check to see if we removed the last address, leaving a terminating comma +that needs to be removed */ + +s = *listptr + Ustrlen(*listptr); +while (s > *listptr && (isspace(s[-1]) || s[-1] == ',')) s--; +*s = 0; + +/* Check to see if there any addresses left; if not, set NULL */ + +s = *listptr; +while (s != 0 && isspace(*s)) s++; +if (*s == 0) *listptr = 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) + { + once_repeat_sec = readconf_readtime(oncerepeat, 0, FALSE); + if (once_repeat_sec < 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) check_never_mail(&to, never_mail); + if (cc) check_never_mail(&cc, never_mail); + if (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, is_tainted(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; + EXIM_DBOPEN(oncelog, dirname, O_RDWR|O_CREAT, ob->mode, &dbm_file); + if (!dbm_file) + { + 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(key_datum) = CS to; + EXIM_DATUM_SIZE(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(result_datum) == sizeof(time_t)) + memcpy(&then, EXIM_DATUM_DATA(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(key_datum) = CS to; + EXIM_DATUM_SIZE(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(value_datum) = CS (&now); + EXIM_DATUM_SIZE(value_datum) = (int)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..5a1b7c9 --- /dev/null +++ b/src/transports/lmtp.c @@ -0,0 +1,809 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* Copyright (c) The Exim Maintainers 2020 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +#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, 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 = (pcre_exec(regex_IGNOREQUOTA, NULL, CS buffer, + Ustrlen(CS buffer), 0, PCRE_EOPT, NULL, 0) >= 0)? 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..27422bd --- /dev/null +++ b/src/transports/pipe.c @@ -0,0 +1,1145 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* Copyright (c) The Exim maintainers 2020 */ +/* See the file NOTICE for conditions of use and distribution. */ + + +#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 = { + NULL, /* cmd */ + NULL, /* allow_commands */ + NULL, /* environment */ + US"/bin:/usr/bin", /* path */ + NULL, /* message_prefix (reset in init if not bsmtp) */ + NULL, /* message_suffix (ditto) */ + US mac_expanded_string(EX_TEMPFAIL) ":" /* temp_errors */ + mac_expanded_string(EX_CANTCREAT), + NULL, /* check_string */ + NULL, /* escape_string */ + 022, /* umask */ + 20480, /* max_output */ + 60*60, /* timeout */ + 0, /* options */ + FALSE, /* force_command */ + FALSE, /* freeze_exec_fail */ + FALSE, /* freeze_signal */ + FALSE, /* ignore_status */ + FALSE, /* permit_coredump */ + FALSE, /* restrict_to_path */ + FALSE, /* timeout_defer */ + FALSE, /* use_shell */ + FALSE, /* use_bsmtp */ + FALSE, /* use_classresources */ + FALSE /* use_crlf */ +}; + + + +/************************************************* +* 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); + +addrlist = addrlist; /* Keep compiler happy */ +dummy = dummy; +uid = uid; +gid = gid; +errmsg = errmsg; +ob = ob; + +#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, 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 *), FALSE); + +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) + { + /*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)) + { + 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 != NULL) + envp[envcount++] = string_sprintf("HOST=%s", addr->host_list->name); + +if (f.timestamps_utc) envp[envcount++] = US"TZ=UTC"; +else if (timezone_string != NULL && timezone_string[0] != 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..9721854 --- /dev/null +++ b/src/transports/queuefile.c @@ -0,0 +1,282 @@ +/************************************************* +* 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 - 2020 */ +/* See the file NOTICE for conditions of use and distribution. */ + + + +#include "../exim.h" + +#ifdef EXPERIMENTAL_QUEUEFILE /* whole file */ +#include "queuefile.h" + +/* 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..f26e233 --- /dev/null +++ b/src/transports/smtp.c @@ -0,0 +1,5640 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* Copyright (c) The Exim Maintainers 2020 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#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) }, + { "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_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) }, + { "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_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) }, +# ifdef EXPERIMENTAL_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 = { + .hosts = NULL, + .fallback_hosts = NULL, + .hostlist = NULL, + .fallback_hostlist = NULL, + .helo_data = US"$primary_hostname", + .interface = NULL, + .port = NULL, + .protocol = US"smtp", + .dscp = NULL, + .serialize_hosts = NULL, + .hosts_try_auth = NULL, + .hosts_require_auth = NULL, + .hosts_try_chunking = US"*", +#ifdef SUPPORT_DANE + .hosts_try_dane = US"*", + .hosts_require_dane = NULL, + .dane_require_tls_ciphers = NULL, +#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()) */ + .hosts_require_ocsp = NULL, +#endif + .hosts_require_tls = NULL, + .hosts_avoid_tls = NULL, + .hosts_verify_avoid_tls = NULL, + .hosts_avoid_pipelining = NULL, +#ifndef DISABLE_PIPE_CONNECT + .hosts_pipe_connect = NULL, +#endif + .hosts_avoid_esmtp = NULL, +#ifndef DISABLE_TLS + .hosts_nopass_tls = NULL, + .hosts_noproxy_tls = NULL, +#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, + .address_retry_include_sender = TRUE, + .allow_localhost = FALSE, + .authenticated_sender_force = FALSE, + .gethostbyname = FALSE, + .dns_qualify_single = TRUE, + .dns_search_parents = FALSE, + .dnssec = { .request= US"*", .require=NULL }, + .delay_after_cutoff = TRUE, + .hosts_override = FALSE, + .hosts_randomize = FALSE, + .keepalive = TRUE, + .lmtp_ignore_quota = FALSE, + .expand_retry_include_ip_address = NULL, + .retry_include_ip_address = TRUE, +#ifdef SUPPORT_SOCKS + .socks_proxy = NULL, +#endif +#ifndef DISABLE_TLS + .tls_certificate = NULL, + .tls_crl = NULL, + .tls_privatekey = NULL, + .tls_require_ciphers = NULL, + .tls_sni = NULL, + .tls_verify_certificates = US"system", + .tls_dh_min_bits = EXIM_CLIENT_DH_DEFAULT_MIN_BITS, + .tls_tempfail_tryclear = TRUE, +# ifdef EXPERIMENTAL_TLS_RESUME + .tls_resumption_hosts = NULL, +# endif + .tls_verify_hosts = NULL, + .tls_try_verify_hosts = US"*", + .tls_verify_cert_hostnames = US"*", +#endif +#ifdef SUPPORT_I18N + .utf8_downconvert = US"-1", +#endif +#ifndef DISABLE_DKIM + .dkim = + {.dkim_domain = NULL, + .dkim_identity = NULL, + .dkim_private_key = NULL, + .dkim_selector = NULL, + .dkim_canon = NULL, + .dkim_sign_headers = NULL, + .dkim_strict = NULL, + .dkim_hash = US"sha256", + .dkim_timestamps = NULL, + .dot_stuffed = FALSE, + .force_bodyhash = FALSE, +# ifdef EXPERIMENTAL_ARC + .arc_signspec = NULL, +# endif + }, +# ifdef EXPERIMENTAL_ARC + .arc_sign = NULL, +# endif +#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) +{ +if (!regex_PIPELINING) regex_PIPELINING = + regex_must_compile(US"\\n250[\\s\\-]PIPELINING(\\s|\\n|$)", FALSE, TRUE); + +if (!regex_SIZE) regex_SIZE = + regex_must_compile(US"\\n250[\\s\\-]SIZE(\\s|\\n|$)", FALSE, TRUE); + +if (!regex_AUTH) regex_AUTH = + regex_must_compile(AUTHS_REGEX, FALSE, TRUE); + +#ifndef DISABLE_TLS +if (!regex_STARTTLS) regex_STARTTLS = + regex_must_compile(US"\\n250[\\s\\-]STARTTLS(\\s|\\n|$)", FALSE, TRUE); +#endif + +if (!regex_CHUNKING) regex_CHUNKING = + regex_must_compile(US"\\n250[\\s\\-]CHUNKING(\\s|\\n|$)", FALSE, TRUE); + +#ifndef DISABLE_PRDR +if (!regex_PRDR) regex_PRDR = + regex_must_compile(US"\\n250[\\s\\-]PRDR(\\s|\\n|$)", FALSE, TRUE); +#endif + +#ifdef SUPPORT_I18N +if (!regex_UTF8) regex_UTF8 = + regex_must_compile(US"\\n250[\\s\\-]SMTPUTF8(\\s|\\n|$)", FALSE, TRUE); +#endif + +if (!regex_DSN) regex_DSN = + regex_must_compile(US"\\n250[\\s\\-]DSN(\\s|\\n|$)", FALSE, TRUE); + +if (!regex_IGNOREQUOTA) regex_IGNOREQUOTA = + regex_must_compile(US"\\n250[\\s\\-]IGNOREQUOTA(\\s|\\n|$)", FALSE, TRUE); + +#ifndef DISABLE_PIPE_CONNECT +if (!regex_EARLY_PIPE) regex_EARLY_PIPE = + regex_must_compile(US"\\n250[\\s\\-]" EARLY_PIPE_FEATURE_NAME "(\\s|\\n|$)", FALSE, TRUE); +#endif +} + + +/************************************************* +* 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; + +errmsg = errmsg; /* Keep picky compilers happy */ +uid = uid; +gid = gid; + +/* 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; + +/* 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 != NULL) 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. */ + +host_build_hostlist(&(ob->fallback_hostlist), ob->fallback_hosts, FALSE); +} + + + + + +/************************************************* +* 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); + +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 = 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); +#endif +return TRUE; +} + + + +#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); +} + +static void +write_ehlo_cache_entry(const smtp_context * sx) +{ +open_db dbblock, * dbm_file; + +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) 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(dbm_file, ehlo_resp_key))) + { 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 + { + sx->ehlo_resp = er->data; + dbfn_close(dbm_file); + DEBUG(D_transport) 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); + 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; + } + } + +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 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 + ); + 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); + *(tls_out.active.sock < 0 + ? &sx->ehlo_resp.cleartext_features : &sx->ehlo_resp.crypted_features) = peer_offered; + *ap = authbits; + if (peer_offered & OPTION_EARLY_PIPE) + write_ehlo_cache_entry(sx); + else + invalidate_ehlo_cache_entry(sx); + + return OK; /* just carry on */ + } + } +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\n", __FUNCTION__); + 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; + +sx->outblock.authenticating = TRUE; +rc = (au->info->clientcode)(au, sx, ob->command_timeout, + sx->buffer, sizeof(sx->buffer)); +sx->outblock.authenticating = FALSE; +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 = deliver_get_sender_address(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) +{ +size_t bsize = Ustrlen(buf); + +/* debug_printf("%s: check for 0x%04x\n", __FUNCTION__, checks); */ + +#ifndef DISABLE_TLS +if ( checks & OPTION_TLS + && pcre_exec(regex_STARTTLS, NULL, CS buf, bsize, 0, PCRE_EOPT, NULL, 0) < 0) +#endif + checks &= ~OPTION_TLS; + +if ( checks & OPTION_IGNQ + && pcre_exec(regex_IGNOREQUOTA, NULL, CS buf, bsize, 0, + PCRE_EOPT, NULL, 0) < 0) + checks &= ~OPTION_IGNQ; + +if ( checks & OPTION_CHUNKING + && pcre_exec(regex_CHUNKING, NULL, CS buf, bsize, 0, PCRE_EOPT, NULL, 0) < 0) + checks &= ~OPTION_CHUNKING; + +#ifndef DISABLE_PRDR +if ( checks & OPTION_PRDR + && pcre_exec(regex_PRDR, NULL, CS buf, bsize, 0, PCRE_EOPT, NULL, 0) < 0) +#endif + checks &= ~OPTION_PRDR; + +#ifdef SUPPORT_I18N +if ( checks & OPTION_UTF8 + && pcre_exec(regex_UTF8, NULL, CS buf, bsize, 0, PCRE_EOPT, NULL, 0) < 0) +#endif + checks &= ~OPTION_UTF8; + +if ( checks & OPTION_DSN + && pcre_exec(regex_DSN, NULL, CS buf, bsize, 0, PCRE_EOPT, NULL, 0) < 0) + checks &= ~OPTION_DSN; + +if ( checks & OPTION_PIPE + && pcre_exec(regex_PIPELINING, NULL, CS buf, bsize, 0, + PCRE_EOPT, NULL, 0) < 0) + checks &= ~OPTION_PIPE; + +if ( checks & OPTION_SIZE + && pcre_exec(regex_SIZE, NULL, CS buf, bsize, 0, PCRE_EOPT, NULL, 0) < 0) + checks &= ~OPTION_SIZE; + +#ifndef DISABLE_PIPE_CONNECT +if ( checks & OPTION_EARLY_PIPE + && pcre_exec(regex_EARLY_PIPE, NULL, CS buf, bsize, 0, + PCRE_EOPT, NULL, 0) < 0) +#endif + checks &= ~OPTION_EARLY_PIPE; + +/* 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; +} + + + + + +/************************************************* +* Make connection for given message * +*************************************************/ + +/* +Arguments: + ctx 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 + +sx->conn_args.ob = ob; + +sx->lmtp = strcmpic(ob->protocol, US"lmtp") == 0; +sx->smtps = strcmpic(ob->protocol, US"smtps") == 0; +/* sx->ok = FALSE; */ +sx->send_rset = TRUE; +sx->send_quit = TRUE; +sx->setting_up = TRUE; +sx->esmtp = TRUE; +/* sx->esmtp_sent = FALSE; */ +#ifdef SUPPORT_I18N +/* sx->utf8_needed = FALSE; */ +#endif +sx->dsn_all_lasthop = TRUE; +#ifdef SUPPORT_DANE +/* sx->conn_args.dane = FALSE; */ +sx->dane_required = + verify_check_given_host(CUSS &ob->hosts_require_dane, sx->conn_args.host) == OK; +#endif +#ifndef DISABLE_PIPE_CONNECT +/* sx->early_pipe_active = sx->early_pipe_ok = FALSE; */ +/* sx->ehlo_resp.cleartext_features = sx->ehlo_resp.crypted_features = 0; */ +/* sx->pending_BANNER = sx->pending_EHLO = sx->pending_MAIL = FALSE; */ +#endif + +if ((sx->max_rcpt = sx->conn_args.tblock->max_addresses) == 0) sx->max_rcpt = 999999; +/* sx->peer_offered = 0; */ +/* sx->avoid_option = 0; */ +sx->igquotstr = US""; +if (!sx->helo_data) sx->helo_data = ob->helo_data; +#ifdef EXPERIMENTAL_DSN_INFO +/* sx->smtp_greeting = NULL; */ +/* sx->helo_response = NULL; */ +#endif + +smtp_command = US"initial connection"; +/* sx->buffer[0] = '\0'; */ + +/* 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; +/* sx->outblock.cmd_count = 0; */ +/* sx->outblock.authenticating = FALSE; */ +/* sx->outblock.conn_args = NULL; */ + +/* 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; +#ifdef EXPERIMENTAL_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 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 + && ( 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->first_addr->domain; /* 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"); +# endif + 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"); + + HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" SMTP>> QUIT\n"); + 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); + + /* 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( 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->first_addr->domain; /* 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"); +# endif + 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"); +# endif + return FAIL; + } + } +#endif /*DANE*/ + + /* Make the TCP connection */ + + sx->cctx.tls_ctx = NULL; + sx->inblock.cctx = sx->outblock.cctx = &sx->cctx; + sx->avoid_option = sx->peer_offered = smtp_peer_options = 0; + +#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 + || !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 PIPE_CONNECT\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\n"); + +PIPE_CONNECT_RETRY: + if (sx->early_pipe_active) + sx->outblock.conn_args = &sx->conn_args; + else +#endif + { + if ((sx->cctx.sock = smtp_connect(&sx->conn_args, 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; + } + } + /* 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_interface and $sending_port are set. */ +/*XXX early-pipe: they still will not be. Is there any way to find out what they +will be? Somehow I doubt it. */ + + 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 + { +#ifdef TCP_QUICKACK + (void) setsockopt(sx->cctx.sock, IPPROTO_TCP, TCP_QUICKACK, US &off, + sizeof(off)); +#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); + 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 + ); +#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("PIPE_CONNECT usable in future for this IP\n"); + sx->ehlo_resp.cleartext_auths = study_ehlo_auths(sx); + write_ehlo_cache_entry(sx); + } + } +#endif + } + + /* 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; + 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. */ + + if (sx->early_pipe_active && 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: + { + 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. */ +#ifdef USE_GNUTLS + GNUTLS_CONN_FAILED: +#endif + 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"); /* 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; + } + + /* TLS session is set up */ + + 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 PIPE_CONNECT\n"); + } +#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 GNUTLS_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"); +# 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 == NULL +#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 + } + + /* 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) + sx->peer_offered &= ~OPTION_CHUNKING; + + if (sx->peer_offered & 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) + sx->peer_offered &= ~OPTION_PRDR; + + if (sx->peer_offered & 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("PIPE_CONNECT 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("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) + { + tls_close(sx->cctx.tls_ctx, TLS_SHUTDOWN_NOWAIT); + 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); +#endif + +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 (sx->peer_offered & 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; + 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; +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; + + 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 */ + +sx->next_addr = addr; +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 + +Does not return. +*/ + +void +smtp_proxy_tls(void * ct_ctx, uschar * buf, size_t bsize, int * pfd, + int timeout) +{ +fd_set rfds, efds; +int max_fd = MAX(pfd[0], tls_out.active.sock) + 1; +int rc, i; + +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"); +FD_ZERO(&rfds); +FD_SET(tls_out.active.sock, &rfds); +FD_SET(pfd[0], &rfds); + +for (int fd_bits = 3; fd_bits; ) + { + time_t time_left = timeout; + time_t time_start = time(NULL); + + /* wait for data */ + efds = rfds; + do + { + struct timeval tv = { time_left, 0 }; + + rc = select(max_fd, + (SELECT_ARG2_TYPE *)&rfds, NULL, (SELECT_ARG2_TYPE *)&efds, &tv); + + 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; + } + + if (FD_ISSET(tls_out.active.sock, &efds) || FD_ISSET(pfd[0], &efds)) + { + DEBUG(D_transport) debug_printf("select: exceptional cond on %s fd\n", + FD_ISSET(pfd[0], &efds) ? "proxy" : "tls"); + goto done; + } + } + while (rc < 0 || !(FD_ISSET(tls_out.active.sock, &rfds) || FD_ISSET(pfd[0], &rfds))); + + /* handle inbound data */ + if (FD_ISSET(tls_out.active.sock, &rfds)) + if ((rc = tls_read(ct_ctx, buf, bsize)) <= 0) + { + fd_bits &= ~1; + FD_CLR(tls_out.active.sock, &rfds); + 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; + } + else if (fd_bits & 1) + FD_SET(tls_out.active.sock, &rfds); + + /* handle outbound data */ + if (FD_ISSET(pfd[0], &rfds)) + if ((rc = read(pfd[0], buf, bsize)) <= 0) + { + fd_bits = 0; + tls_close(ct_ctx, TLS_SHUTDOWN_NOWAIT); + ct_ctx = NULL; + } + else + { + for (int nbytes = 0; rc - nbytes > 0; nbytes += i) + if ((i = tls_write(ct_ctx, buf + nbytes, rc - nbytes, FALSE)) < 0) + goto done; + } + else if (fd_bits & 2) + FD_SET(pfd[0], &rfds); + } + +done: + 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; + +BOOL pass_message = FALSE; +uschar *message = NULL; +uschar new_message_id[MESSAGE_ID_LENGTH + 1]; +smtp_context * sx = store_get(sizeof(*sx), TRUE); /* tainted, for the data buffers */ +#ifdef SUPPORT_DANE +BOOL dane_held; +#endif + +suppress_tls = suppress_tls; /* stop compiler warning when no TLS support */ +*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->verify = FALSE; */ +gettimeofday(&sx->delivery_start, NULL); +sx->sync_addr = sx->first_addr = addrlist; + +#ifdef SUPPORT_DANE +DANE_DOMAINS: +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, + string_sprintf("%.50s transport", 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 + && sx->peer_offered & 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 + ) + { + sx->peer_offered &= ~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 ( !(sx->peer_offered & 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 + && (!(sx->peer_offered & 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 (sx->peer_offered & 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 (sx->peer_offered & OPTION_CHUNKING) + debug_printf(" will write message using CHUNKING\n"); + else + debug_printf(" SMTP>> writing message and terminating \".\"\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 + } + 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 (sx->peer_offered & 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 (sx->peer_offered & 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\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)); + } + } + +#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) + 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; + 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("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; + + 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; + } + } + + /* 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) + { + BOOL more; + smtp_compare_t t_compare; + + t_compare.tblock = tblock; + t_compare.current_sender_address = sender_address; + + if ( sx->first_addr != NULL /* more addrs for this message */ + || f.continue_more /* more addrs for coninued-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, + tblock->connection_max_messages, new_message_id, &more, + (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("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 != NULL) /* More addresses still to be sent */ + { /* for this message */ + continue_sequence++; /* 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, TLS_SHUTDOWN_WAIT); + 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)) + { + 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); + } + + 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. There has been discussion on the net about what +to do after sending QUIT. The wording of the RFC suggests that it is necessary +to wait for a response, but on the other hand, there isn't anything one can do +with an error response, other than log it. Exim used to do that. However, +further discussion suggested that it is positively advantageous not to wait for +the response, but to close the session immediately. This is supposed to move +the TCP/IP TIME_WAIT state from the server to the client, thereby removing some +load from the server. (Hosts that are both servers and clients may not see much +difference, of course.) Further discussion indicated that this was safe to do +on Unix systems which have decent implementations of TCP/IP that leave the +connection around for a while (TIME_WAIT) after the application has gone away. +This enables the response sent by the server to be properly ACKed rather than +timed out, as can happen on broken TCP/IP implementations on other OS. + +This change is being made on 31-Jul-98. After over a year of trouble-free +operation, the old commented-out code was removed on 17-Sep-99. */ + +SEND_QUIT: +#ifdef TCP_CORK +(void) setsockopt(sx->cctx.sock, IPPROTO_TCP, TCP_CORK, US &on, sizeof(on)); +#endif +if (sx->send_quit) (void)smtp_write_command(sx, SCMD_FLUSH, "QUIT\r\n"); + +END_OFF: + +#ifndef DISABLE_TLS +tls_close(sx->cctx.tls_ctx, TLS_SHUTDOWN_NOWAIT); +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, + +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. */ + +HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" SMTP(close)>>\n"); +if (sx->send_quit) + { + shutdown(sx->cctx.sock, SHUT_WR); + millisleep(20); + testharness_pause_ms(200); + 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 */ + } +(void)close(sx->cctx.sock); + +#ifndef DISABLE_EVENT +(void) event_raise(tblock->event_action, US"tcp:close", 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; + DEBUG(D_transport) debug_printf("DANE: go-around for %s\n", a->domain); + } + } + goto DANE_DOMAINS; + } +#endif + +continue_transport = NULL; +continue_hostname = NULL; +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); + } + +/* 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, '$') != NULL) + { + 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, ':') == NULL ? AF_INET : AF_INET6; + { + 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), FALSE); + *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..213bca1 --- /dev/null +++ b/src/transports/smtp.h @@ -0,0 +1,209 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* Copyright (c) The Exim Maintainers 2020 */ +/* See the file NOTICE for conditions of use and distribution. */ + +#define DELIVER_BUFFER_SIZE 4096 + +#define PENDING 256 +#define PENDING_DEFER (PENDING + DEFER) +#define PENDING_OK (PENDING + OK) + + +/* 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_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; + 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_certificate; + uschar *tls_crl; + uschar *tls_privatekey; + uschar *tls_require_ciphers; +# ifdef EXPERIMENTAL_TLS_RESUME + 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 +} 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; + BOOL completed_addr:1; + BOOL send_rset:1; + BOOL send_quit:1; + + int max_rcpt; + int cmd_count; + + unsigned peer_offered; + unsigned avoid_option; + uschar * igquotstr; + uschar * helo_data; +#ifdef EXPERIMENTAL_DSN_INFO + uschar * smtp_greeting; + uschar * helo_response; +#endif +#ifndef DISABLE_PIPE_CONNECT + 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..cd8ed3e --- /dev/null +++ b/src/transports/smtp_socks.c @@ -0,0 +1,418 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* 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; +static BOOL srandomed = FALSE; + +if (nproxies == 1) /* shortcut, if we have only 1 server */ + return (proxies[0].is_failed ? -1 : 0); + +/* init random */ +if (!srandomed) + { + struct timeval tv; + gettimeofday(&tv, NULL); + srandom((unsigned int)(tv.tv_usec/1000)); + srandomed = TRUE; + } + +/* 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() % 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; +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); + } + +/* 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; + int proxy_af; + + 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_af = Ustrchr(sob->proxy_host, ':') ? AF_INET6 : AF_INET; + + /*XXX we trust that the method-select command is idempotent */ + if ((fd = smtp_sock_connect(&proxy, proxy_af, sob->port, + interface, tb, 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..cd1964a --- /dev/null +++ b/src/transports/tf_maildir.c @@ -0,0 +1,584 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2018 */ +/* Copyright (c) The Exim Maintainers 2020 */ +/* See the file NOTICE for conditions of use and distribution. */ + +/* 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 != NULL) + { + const uschar *error; + int offset; + const pcre *regex; + + DEBUG(D_transport) debug_printf("checking for maildirfolder requirement\n"); + + if (!(regex = pcre_compile(CS maildirfolder_create_regex, PCRE_COPT, + CCSS &error, &offset, NULL))) + { + addr->message = string_sprintf("appendfile: regular expression " + "error: %s at offset %d while compiling %s", error, offset, + maildirfolder_create_regex); + return FALSE; + } + + if (pcre_exec(regex, NULL, CS path, Ustrlen(path), 0, 0, NULL, 0) >= 0) + { + 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 pcre *regex, const pcre *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 != NULL && + pcre_exec(dir_regex, NULL, CS name, Ustrlen(name), 0, 0, NULL, 0) < 0) + { + 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 pcre *regex, const pcre *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..0be6bc4 --- /dev/null +++ b/src/transports/tf_maildir.h @@ -0,0 +1,20 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Copyright (c) University of Cambridge 1995 - 2009 */ +/* 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 pcre *, + const pcre *, BOOL); +extern BOOL maildir_ensure_directories(uschar *, address_item *, BOOL, int, + uschar *); +extern int maildir_ensure_sizefile(uschar *, + appendfile_transport_options_block *, const pcre *, + const pcre *, off_t *, int *); +extern void maildir_record_length(int, int); + +/* End of tf_maildir.h */ |