summaryrefslogtreecommitdiffstats
path: root/src/transports/appendfile.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/transports/appendfile.c')
-rw-r--r--src/transports/appendfile.c3317
1 files changed, 3317 insertions, 0 deletions
diff --git a/src/transports/appendfile.c b/src/transports/appendfile.c
new file mode 100644
index 0000000..93281ef
--- /dev/null
+++ b/src/transports/appendfile.c
@@ -0,0 +1,3317 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) The Exim maintainers 2020 - 2022 */
+/* Copyright (c) University of Cambridge 1995 - 2020 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+
+#include "../exim.h"
+#include "appendfile.h"
+
+#ifdef SUPPORT_MAILDIR
+#include "tf_maildir.h"
+#endif
+
+
+/* Options specific to the appendfile transport. They must be in alphabetic
+order (note that "_" comes before the lower case letters). Some of them are
+stored in the publicly visible instance block - these are flagged with the
+opt_public flag. */
+#define LOFF(field) OPT_OFF(appendfile_transport_options_block, field)
+
+optionlist appendfile_transport_options[] = {
+#ifdef SUPPORT_MAILDIR
+ { "*expand_maildir_use_size_file", opt_stringptr, LOFF(expand_maildir_use_size_file) },
+#endif
+ { "*set_use_fcntl_lock",opt_bool | opt_hidden, LOFF(set_use_fcntl) },
+ { "*set_use_flock_lock",opt_bool | opt_hidden, LOFF(set_use_flock) },
+ { "*set_use_lockfile", opt_bool | opt_hidden, LOFF(set_use_lockfile) },
+#ifdef SUPPORT_MBX
+ { "*set_use_mbx_lock", opt_bool | opt_hidden, LOFF(set_use_mbx_lock) },
+#endif
+ { "allow_fifo", opt_bool, LOFF(allow_fifo) },
+ { "allow_symlink", opt_bool, LOFF(allow_symlink) },
+ { "batch_id", opt_stringptr | opt_public, OPT_OFF(transport_instance, batch_id) },
+ { "batch_max", opt_int | opt_public, OPT_OFF(transport_instance, batch_max) },
+ { "check_group", opt_bool, LOFF(check_group) },
+ { "check_owner", opt_bool, LOFF(check_owner) },
+ { "check_string", opt_stringptr, LOFF(check_string) },
+ { "create_directory", opt_bool, LOFF(create_directory) },
+ { "create_file", opt_stringptr, LOFF(create_file_string) },
+ { "directory", opt_stringptr, LOFF(dirname) },
+ { "directory_file", opt_stringptr, LOFF(dirfilename) },
+ { "directory_mode", opt_octint, LOFF(dirmode) },
+ { "escape_string", opt_stringptr, LOFF(escape_string) },
+ { "file", opt_stringptr, LOFF(filename) },
+ { "file_format", opt_stringptr, LOFF(file_format) },
+ { "file_must_exist", opt_bool, LOFF(file_must_exist) },
+ { "lock_fcntl_timeout", opt_time, LOFF(lock_fcntl_timeout) },
+ { "lock_flock_timeout", opt_time, LOFF(lock_flock_timeout) },
+ { "lock_interval", opt_time, LOFF(lock_interval) },
+ { "lock_retries", opt_int, LOFF(lock_retries) },
+ { "lockfile_mode", opt_octint, LOFF(lockfile_mode) },
+ { "lockfile_timeout", opt_time, LOFF(lockfile_timeout) },
+ { "mailbox_filecount", opt_stringptr, LOFF(mailbox_filecount_string) },
+ { "mailbox_size", opt_stringptr, LOFF(mailbox_size_string) },
+#ifdef SUPPORT_MAILDIR
+ { "maildir_format", opt_bool, LOFF(maildir_format ) } ,
+ { "maildir_quota_directory_regex", opt_stringptr, LOFF(maildir_dir_regex) },
+ { "maildir_retries", opt_int, LOFF(maildir_retries) },
+ { "maildir_tag", opt_stringptr, LOFF(maildir_tag) },
+ { "maildir_use_size_file", opt_expand_bool, LOFF(maildir_use_size_file ) } ,
+ { "maildirfolder_create_regex", opt_stringptr, LOFF(maildirfolder_create_regex ) },
+#endif /* SUPPORT_MAILDIR */
+#ifdef SUPPORT_MAILSTORE
+ { "mailstore_format", opt_bool, LOFF(mailstore_format ) },
+ { "mailstore_prefix", opt_stringptr, LOFF(mailstore_prefix ) },
+ { "mailstore_suffix", opt_stringptr, LOFF(mailstore_suffix ) },
+#endif /* SUPPORT_MAILSTORE */
+#ifdef SUPPORT_MBX
+ { "mbx_format", opt_bool, LOFF(mbx_format ) } ,
+#endif /* SUPPORT_MBX */
+ { "message_prefix", opt_stringptr, LOFF(message_prefix) },
+ { "message_suffix", opt_stringptr, LOFF(message_suffix) },
+ { "mode", opt_octint, LOFF(mode) },
+ { "mode_fail_narrower",opt_bool, LOFF(mode_fail_narrower) },
+ { "notify_comsat", opt_bool, LOFF(notify_comsat) },
+ { "quota", opt_stringptr, LOFF(quota) },
+ { "quota_directory", opt_stringptr, LOFF(quota_directory) },
+ { "quota_filecount", opt_stringptr, LOFF(quota_filecount) },
+ { "quota_is_inclusive", opt_bool, LOFF(quota_is_inclusive) },
+ { "quota_size_regex", opt_stringptr, LOFF(quota_size_regex) },
+ { "quota_warn_message", opt_stringptr | opt_public, OPT_OFF(transport_instance, warn_message) },
+ { "quota_warn_threshold", opt_stringptr, LOFF(quota_warn_threshold) },
+ { "use_bsmtp", opt_bool, LOFF(use_bsmtp) },
+ { "use_crlf", opt_bool, LOFF(use_crlf) },
+ { "use_fcntl_lock", opt_bool_set, LOFF(use_fcntl) },
+ { "use_flock_lock", opt_bool_set, LOFF(use_flock) },
+ { "use_lockfile", opt_bool_set, LOFF(use_lockfile) },
+#ifdef SUPPORT_MBX
+ { "use_mbx_lock", opt_bool_set, LOFF(use_mbx_lock) },
+#endif /* SUPPORT_MBX */
+};
+
+/* Size of the options list. An extern variable has to be used so that its
+address can appear in the tables drtables.c. */
+
+int appendfile_transport_options_count =
+ sizeof(appendfile_transport_options)/sizeof(optionlist);
+
+
+#ifdef MACRO_PREDEF
+
+/* Dummy values */
+appendfile_transport_options_block appendfile_transport_option_defaults = {0};
+void appendfile_transport_init(transport_instance *tblock) {}
+BOOL appendfile_transport_entry(transport_instance *tblock, address_item *addr) {return FALSE;}
+
+#else /*!MACRO_PREDEF*/
+
+/* Default private options block for the appendfile transport. */
+
+appendfile_transport_options_block appendfile_transport_option_defaults = {
+ /* all non-mentioned members zero/null/false */
+ .dirfilename = US"q${base62:$tod_epoch}-$inode",
+ .create_file_string = US"anywhere",
+ .maildir_dir_regex = US"^(?:cur|new|\\..*)$",
+ .mailbox_size_value = -1,
+ .mailbox_filecount_value = -1,
+ .mode = APPENDFILE_MODE,
+ .dirmode = APPENDFILE_DIRECTORY_MODE,
+ .lockfile_mode = APPENDFILE_LOCKFILE_MODE,
+ .lockfile_timeout = 30*60,
+ .lock_retries = 10,
+ .lock_interval = 3,
+ .maildir_retries = 10,
+ .create_file = create_anywhere,
+ .check_owner = TRUE,
+ .create_directory = TRUE,
+ .notify_comsat = FALSE,
+ .use_lockfile = TRUE,
+ .use_fcntl = TRUE,
+ .mode_fail_narrower = TRUE,
+ .quota_is_inclusive = TRUE,
+};
+
+
+/* Encodings for mailbox formats, and their names. MBX format is actually
+supported only if SUPPORT_MBX is set. */
+
+enum { mbf_unix, mbf_mbx, mbf_smail, mbf_maildir, mbf_mailstore };
+
+static const char *mailbox_formats[] = {
+ "unix", "mbx", "smail", "maildir", "mailstore" };
+
+
+/* Check warn threshold only if quota size set or not a percentage threshold
+ percentage check should only be done if quota > 0 */
+
+#define THRESHOLD_CHECK (ob->quota_warn_threshold_value > 0 && \
+ (!ob->quota_warn_threshold_is_percent || ob->quota_value > 0))
+
+
+
+/*************************************************
+* Setup entry point *
+*************************************************/
+
+/* Called for each delivery in the privileged state, just before the uid/gid
+are changed and the main entry point is called. We use this function to
+expand any quota settings, so that it can access files that may not be readable
+by the user. It is also used to pick up external mailbox size information, if
+set.
+
+Arguments:
+ tblock points to the transport instance
+ addrlist addresses about to be delivered (not used)
+ dummy not used (doesn't pass back data)
+ uid the uid that will be set (not used)
+ gid the gid that will be set (not used)
+ errmsg where to put an error message
+
+Returns: OK, FAIL, or DEFER
+*/
+
+static int
+appendfile_transport_setup(transport_instance *tblock, address_item *addrlist,
+ transport_feedback *dummy, uid_t uid, gid_t gid, uschar **errmsg)
+{
+appendfile_transport_options_block *ob =
+ (appendfile_transport_options_block *)(tblock->options_block);
+uschar *q = ob->quota;
+double default_value = 0.0;
+
+if (ob->expand_maildir_use_size_file)
+ ob->maildir_use_size_file = expand_check_condition(ob->expand_maildir_use_size_file,
+ US"`maildir_use_size_file` in transport", tblock->name);
+
+/* Loop for quota, quota_filecount, quota_warn_threshold, mailbox_size,
+mailbox_filecount */
+
+for (int i = 0; i < 5; i++)
+ {
+ double d = default_value;
+ int no_check = 0;
+ uschar *which = NULL;
+
+ if (q)
+ {
+ uschar * rest, * s;
+
+ if (!(s = expand_string(q)))
+ {
+ *errmsg = string_sprintf("Expansion of \"%s\" in %s transport failed: "
+ "%s", q, tblock->name, expand_string_message);
+ return f.search_find_defer ? DEFER : FAIL;
+ }
+
+ d = Ustrtod(s, &rest);
+
+ /* Handle following characters K, M, G, %, the latter being permitted
+ for quota_warn_threshold only. A threshold with no quota setting is
+ just ignored. */
+
+ if (tolower(*rest) == 'k') { d *= 1024.0; rest++; }
+ else if (tolower(*rest) == 'm') { d *= 1024.0*1024.0; rest++; }
+ else if (tolower(*rest) == 'g') { d *= 1024.0*1024.0*1024.0; rest++; }
+ else if (*rest == '%' && i == 2)
+ {
+ if (ob->quota_value <= 0 && !ob->maildir_use_size_file)
+ d = 0;
+ else if ((int)d < 0 || (int)d > 100)
+ {
+ *errmsg = string_sprintf("Invalid quota_warn_threshold percentage (%d)"
+ " for %s transport", (int)d, tblock->name);
+ return FAIL;
+ }
+ ob->quota_warn_threshold_is_percent = TRUE;
+ rest++;
+ }
+
+
+ /* For quota and quota_filecount there may be options
+ appended. Currently only "no_check", so we can be lazy parsing it */
+ if (i < 2 && Ustrstr(rest, "/no_check") == rest)
+ {
+ no_check = 1;
+ rest += sizeof("/no_check") - 1;
+ }
+
+ Uskip_whitespace(&rest);
+
+ if (*rest)
+ {
+ *errmsg = string_sprintf("Malformed value \"%s\" (expansion of \"%s\") "
+ "in %s transport", s, q, tblock->name);
+ return FAIL;
+ }
+ }
+
+ /* Set each value, checking for possible overflow. */
+
+ switch (i)
+ {
+ case 0:
+ if (d >= 2.0*1024.0*1024.0*1024.0 && sizeof(off_t) <= 4)
+ which = US"quota";
+ ob->quota_value = (off_t)d;
+ ob->quota_no_check = no_check;
+ q = ob->quota_filecount;
+ break;
+
+ case 1:
+ if (d >= 2.0*1024.0*1024.0*1024.0)
+ which = US"quota_filecount";
+ ob->quota_filecount_value = (int)d;
+ ob->quota_filecount_no_check = no_check;
+ q = ob->quota_warn_threshold;
+ break;
+
+ case 2:
+ if (d >= 2.0*1024.0*1024.0*1024.0 && sizeof(off_t) <= 4)
+ which = US"quota_warn_threshold";
+ ob->quota_warn_threshold_value = (off_t)d;
+ q = ob->mailbox_size_string;
+ default_value = -1.0;
+ break;
+
+ case 3:
+ if (d >= 2.0*1024.0*1024.0*1024.0 && sizeof(off_t) <= 4)
+ which = US"mailbox_size";;
+ ob->mailbox_size_value = (off_t)d;
+ q = ob->mailbox_filecount_string;
+ break;
+
+ case 4:
+ if (d >= 2.0*1024.0*1024.0*1024.0)
+ which = US"mailbox_filecount";
+ ob->mailbox_filecount_value = (int)d;
+ break;
+ }
+
+ if (which)
+ {
+ *errmsg = string_sprintf("%s value %.10g is too large (overflow) in "
+ "%s transport", which, d, tblock->name);
+ return FAIL;
+ }
+ }
+
+return OK;
+}
+
+
+
+/*************************************************
+* Initialization entry point *
+*************************************************/
+
+/* Called for each instance, after its options have been read, to
+enable consistency checks to be done, or anything else that needs
+to be set up. */
+
+void
+appendfile_transport_init(transport_instance *tblock)
+{
+appendfile_transport_options_block *ob =
+ (appendfile_transport_options_block *)(tblock->options_block);
+uschar * s;
+
+/* Set up the setup entry point, to be called in the privileged state */
+
+tblock->setup = appendfile_transport_setup;
+
+/* Lock_retries must be greater than zero */
+
+if (ob->lock_retries == 0) ob->lock_retries = 1;
+
+/* Only one of a file name or directory name must be given. */
+
+if (ob->filename && ob->dirname)
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s transport:\n "
+ "only one of \"file\" or \"directory\" can be specified", tblock->name);
+
+/* If a file name was specified, neither quota_filecount nor quota_directory
+must be given. */
+
+if (ob->filename)
+ {
+ if (ob->quota_filecount)
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s transport:\n "
+ "quota_filecount must not be set without \"directory\"", tblock->name);
+ if (ob->quota_directory)
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s transport:\n "
+ "quota_directory must not be set without \"directory\"", tblock->name);
+ }
+
+/* The default locking depends on whether MBX is set or not. Change the
+built-in default if none of the lock options has been explicitly set. At least
+one form of locking is required in all cases, but mbx locking changes the
+meaning of fcntl and flock locking. */
+
+/* Not all operating systems provide flock(). For those that do, if flock is
+requested, the default for fcntl is FALSE. */
+
+if (ob->use_flock)
+ {
+ #ifdef NO_FLOCK
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s transport:\n "
+ "flock() support was not available in the operating system when this "
+ "binary was built", tblock->name);
+ #endif /* NO_FLOCK */
+ if (!ob->set_use_fcntl) ob->use_fcntl = FALSE;
+ }
+
+#ifdef SUPPORT_MBX
+if (ob->mbx_format)
+ if (!ob->set_use_lockfile && !ob->set_use_fcntl && !ob->set_use_flock &&
+ !ob->set_use_mbx_lock)
+ {
+ ob->use_lockfile = ob->use_flock = FALSE;
+ ob->use_mbx_lock = ob->use_fcntl = TRUE;
+ }
+ else if (ob->use_mbx_lock)
+ {
+ if (!ob->set_use_lockfile) ob->use_lockfile = FALSE;
+ if (!ob->set_use_fcntl) ob->use_fcntl = FALSE;
+ if (!ob->set_use_flock) ob->use_flock = FALSE;
+ if (!ob->use_fcntl && !ob->use_flock) ob->use_fcntl = TRUE;
+ }
+#endif /* SUPPORT_MBX */
+
+if (!ob->use_fcntl && !ob->use_flock && !ob->use_lockfile && !ob->use_mbx_lock)
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s transport:\n "
+ "no locking configured", tblock->name);
+
+/* Unset timeouts for non-used locking types */
+
+if (!ob->use_fcntl) ob->lock_fcntl_timeout = 0;
+if (!ob->use_flock) ob->lock_flock_timeout = 0;
+
+/* If a directory name was specified, only one of maildir or mailstore may be
+specified, and if quota_filecount or quota_directory is given, quota must
+be set. */
+
+if (ob->dirname)
+ {
+ if (ob->maildir_format && ob->mailstore_format)
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s transport:\n "
+ "only one of maildir and mailstore may be specified", tblock->name);
+ if (ob->quota_filecount != NULL && ob->quota == NULL)
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s transport:\n "
+ "quota must be set if quota_filecount is set", tblock->name);
+ if (ob->quota_directory != NULL && ob->quota == NULL)
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s transport:\n "
+ "quota must be set if quota_directory is set", tblock->name);
+ }
+
+/* If a fixed uid field is set, then a gid field must also be set. */
+
+if (tblock->uid_set && !tblock->gid_set && !tblock->expand_gid)
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG,
+ "user set without group for the %s transport", tblock->name);
+
+/* If "create_file" is set, check that a valid option is given, and set the
+integer variable. */
+
+if ((s = ob->create_file_string ) && *s)
+ {
+ int val = 0;
+ if (Ustrcmp(s, "anywhere") == 0) val = create_anywhere;
+ else if (*s == '/' || Ustrcmp(s, "belowhome") == 0) val = create_belowhome;
+ else if (Ustrcmp(s, "inhome") == 0) val = create_inhome;
+ else
+ log_write(0, LOG_PANIC_DIE|LOG_CONFIG,
+ "invalid value given for \"create_file\" for the %s transport: '%s'",
+ tblock->name, s);
+ ob->create_file = val;
+ }
+
+/* If quota_warn_threshold is set, set up default for warn_message. It may
+not be used if the actual threshold for a given delivery ends up as zero,
+of if it's given as a percentage and there's no quota setting. */
+
+if (ob->quota_warn_threshold)
+ {
+ if (!tblock->warn_message) tblock->warn_message = US
+ "To: $local_part@$domain\n"
+ "Subject: Your mailbox\n\n"
+ "This message is automatically created by mail delivery software (Exim).\n\n"
+ "The size of your mailbox has exceeded a warning threshold that is\n"
+ "set by the system administrator.\n";
+ }
+
+/* If batch SMTP is set, force the check and escape strings, and arrange that
+headers are also escaped. */
+
+if (ob->use_bsmtp)
+ {
+ ob->check_string = US".";
+ ob->escape_string = US"..";
+ ob->options |= topt_escape_headers;
+ }
+
+/* If not batch SMTP, not maildir, not mailstore, and directory is not set,
+insert default values for for the affixes and the check/escape strings. */
+
+else if (!ob->dirname && !ob->maildir_format && !ob->mailstore_format)
+ {
+ if (!ob->message_prefix) ob->message_prefix =
+ US"From ${if def:return_path{$return_path}{MAILER-DAEMON}} ${tod_bsdinbox}\n";
+ if (!ob->message_suffix) ob->message_suffix = US"\n";
+ if (!ob->check_string) ob->check_string = US"From ";
+ if (!ob->escape_string) ob->escape_string = US">From ";
+
+ }
+
+/* Set up the bitwise options for transport_write_message from the various
+driver options. Only one of body_only and headers_only can be set. */
+
+ob->options |=
+ (tblock->body_only ? topt_no_headers : 0) |
+ (tblock->headers_only ? topt_no_body : 0) |
+ (tblock->return_path_add ? topt_add_return_path : 0) |
+ (tblock->delivery_date_add ? topt_add_delivery_date : 0) |
+ (tblock->envelope_to_add ? topt_add_envelope_to : 0) |
+ ((ob->use_crlf || ob->mbx_format) ? topt_use_crlf : 0);
+}
+
+
+
+/*************************************************
+* Notify comsat *
+*************************************************/
+
+/* The comsat daemon is the thing that provides asynchronous notification of
+the arrival of local messages, if requested by the user by "biff y". It is a
+BSD thing that uses a TCP/IP protocol for communication. A message consisting
+of the text "user@offset" must be sent, where offset is the place in the
+mailbox where new mail starts. There is no scope for telling it which file to
+look at, which makes it a less than useful if mail is being delivered into a
+non-standard place such as the user's home directory. In fact, it doesn't seem
+to pay much attention to the offset.
+
+Arguments:
+ user user name
+ offset offset in mailbox
+
+Returns: nothing
+*/
+
+static void
+notify_comsat(uschar *user, off_t offset)
+{
+struct servent *sp;
+host_item host;
+uschar * s;
+
+DEBUG(D_transport) debug_printf("notify_comsat called\n");
+
+s = string_sprintf("%.200s@" OFF_T_FMT "\n", user, offset);
+
+if ((sp = getservbyname("biff", "udp")) == NULL)
+ {
+ DEBUG(D_transport) debug_printf("biff/udp is an unknown service");
+ return;
+ }
+
+host.name = US"localhost";
+host.next = NULL;
+
+
+/* This code is all set up to look up "localhost" and use all its addresses
+until one succeeds. However, it appears that at least on some systems, comsat
+doesn't listen on the ::1 address. So for the moment, just force the address to
+be 127.0.0.1. At some future stage, when IPv6 really is superseding IPv4, this
+can be changed. (But actually, comsat is probably dying out anyway.) */
+
+/******
+if (host_find_byname(&host, NULL, 0, NULL, FALSE) == HOST_FIND_FAILED)
+ {
+ DEBUG(D_transport) debug_printf("\"localhost\" unknown\n");
+ return;
+ }
+******/
+
+host.address = US"127.0.0.1";
+
+
+for (host_item * h = &host; h; h = h->next)
+ {
+ int sock, rc;
+ int host_af = Ustrchr(h->address, ':') != NULL ? AF_INET6 : AF_INET;
+
+ DEBUG(D_transport) debug_printf("calling comsat on %s\n", h->address);
+
+ if ((sock = ip_socket(SOCK_DGRAM, host_af)) < 0) continue;
+
+ /* Connect never fails for a UDP socket, so don't set a timeout. */
+
+ (void)ip_connect(sock, host_af, h->address, ntohs(sp->s_port), 0, NULL);
+ rc = send(sock, s, Ustrlen(s) + 1, 0);
+ (void)close(sock);
+
+ if (rc >= 0) break;
+ DEBUG(D_transport)
+ debug_printf("send to comsat failed for %s: %s\n", strerror(errno),
+ h->address);
+ }
+}
+
+
+
+/*************************************************
+* Check the format of a file *
+*************************************************/
+
+/* This function is called when file_format is set, to check that an existing
+file has the right format. The format string contains text/transport pairs. The
+string matching is literal. we just read big_buffer_size bytes, because this is
+all about the first few bytes of a file.
+
+Arguments:
+ cfd the open file
+ tblock the transport block
+ addr the address block - for inserting error data
+
+Returns: pointer to the required transport, or NULL
+*/
+
+transport_instance *
+check_file_format(int cfd, transport_instance *tblock, address_item *addr)
+{
+const uschar *format =
+ ((appendfile_transport_options_block *)(tblock->options_block))->file_format;
+uschar data[256];
+int len = read(cfd, data, sizeof(data));
+int sep = 0;
+uschar *s;
+
+DEBUG(D_transport) debug_printf("checking file format\n");
+
+/* An empty file matches the current transport */
+
+if (len == 0) return tblock;
+
+/* Search the formats for a match */
+
+/* not expanded so cannot be tainted */
+while ((s = string_nextinlist(&format, &sep, big_buffer, big_buffer_size)))
+ {
+ int slen = Ustrlen(s);
+ BOOL match = len >= slen && Ustrncmp(data, s, slen) == 0;
+ uschar *tp = string_nextinlist(&format, &sep, big_buffer, big_buffer_size);
+
+ if (match && tp)
+ {
+ for (transport_instance * tt = transports; tt; tt = tt->next)
+ if (Ustrcmp(tp, tt->name) == 0)
+ {
+ DEBUG(D_transport)
+ debug_printf("file format -> %s transport\n", tt->name);
+ return tt;
+ }
+ addr->basic_errno = ERRNO_BADTRANSPORT;
+ addr->message = string_sprintf("%s transport (for %.*s format) not found",
+ tp, slen, data);
+ return NULL;
+ }
+ }
+
+/* Failed to find a match */
+
+addr->basic_errno = ERRNO_FORMATUNKNOWN;
+addr->message = US"mailbox file format unrecognized";
+return NULL;
+}
+
+
+
+
+/*************************************************
+* Check directory's files for quota *
+*************************************************/
+
+/* This function is called if quota is set for one of the delivery modes that
+delivers into a specific directory. It scans the directory and stats all the
+files in order to get a total size and count. This is an expensive thing to do,
+but some people are prepared to bear the cost. Alternatively, if size_regex is
+set, it is used as a regex to try to extract the size from the file name, a
+strategy that some people use on maildir files on systems where the users have
+no shell access.
+
+The function is global, because it is also called from tf_maildir.c for maildir
+folders (which should contain only regular files).
+
+Note: Any problems can be written to debugging output, but cannot be written to
+the log, because we are running as an unprivileged user here.
+
+Arguments:
+ dirname the name of the directory
+ countptr where to add the file count (because this function recurses)
+ re a compiled regex to get the size from a name
+
+Returns: the sum of the sizes of the stattable files
+ zero if the directory cannot be opened
+*/
+
+off_t
+check_dir_size(const uschar * dirname, int * countptr, const pcre2_code * re)
+{
+DIR *dir;
+off_t sum = 0;
+int count = *countptr;
+
+if (!(dir = exim_opendir(dirname))) return 0;
+
+for (struct dirent *ent; ent = readdir(dir); )
+ {
+ uschar * path, * name = US ent->d_name;
+ struct stat statbuf;
+
+ if (Ustrcmp(name, ".") == 0 || Ustrcmp(name, "..") == 0) continue;
+
+ count++;
+
+ /* If there's a regex, try to find the size using it */
+
+ if (re)
+ {
+ pcre2_match_data * md = pcre2_match_data_create(2, pcre_gen_ctx);
+ int rc = pcre2_match(re, (PCRE2_SPTR)name, PCRE2_ZERO_TERMINATED,
+ 0, 0, md, pcre_mtc_ctx);
+ PCRE2_SIZE * ovec = pcre2_get_ovector_pointer(md);
+ if ( rc >= 0
+ && (rc = pcre2_get_ovector_count(md)) >= 2)
+ {
+ uschar *endptr;
+ off_t size = (off_t)Ustrtod(name + ovec[2], &endptr);
+ if (endptr == name + ovec[3])
+ {
+ sum += size;
+ DEBUG(D_transport)
+ debug_printf("check_dir_size: size from %s is " OFF_T_FMT "\n", name,
+ size);
+ continue;
+ }
+ }
+ DEBUG(D_transport)
+ debug_printf("check_dir_size: regex did not match %s\n", name);
+ }
+
+ /* No regex or no match for the regex, or captured non-digits */
+
+ path = string_sprintf("%s/%s", dirname, name);
+
+ if (Ustat(path, &statbuf) < 0)
+ {
+ DEBUG(D_transport)
+ debug_printf("check_dir_size: stat error %d for %s: %s\n", errno, path,
+ strerror(errno));
+ }
+ else
+ if ((statbuf.st_mode & S_IFMT) == S_IFREG)
+ sum += statbuf.st_size / statbuf.st_nlink;
+ else if ((statbuf.st_mode & S_IFMT) == S_IFDIR)
+ sum += check_dir_size(path, &count, re);
+ }
+
+closedir(dir);
+DEBUG(D_transport)
+ debug_printf("check_dir_size: dir=%s sum=" OFF_T_FMT " count=%d\n", dirname,
+ sum, count);
+
+*countptr = count;
+return sum;
+}
+
+
+
+
+/*************************************************
+* Apply a lock to a file descriptor *
+*************************************************/
+
+/* This function applies a lock to a file descriptor, using a blocking or
+non-blocking lock, depending on the timeout value. It can apply either or
+both of a fcntl() and a flock() lock. However, not all OS support flock();
+for those that don't, the use_flock option cannot be set.
+
+Arguments:
+ fd the file descriptor
+ fcntltype type of lock, specified as F_WRLCK or F_RDLCK (that is, in
+ fcntl() format); the flock() type is deduced if needed
+ dofcntl do fcntl() locking
+ fcntltime non-zero to use blocking fcntl()
+ doflock do flock() locking
+ flocktime non-zero to use blocking flock()
+
+Returns: yield of the fcntl() or flock() call, with errno preserved;
+ sigalrm_seen set if there has been a timeout
+*/
+
+static int
+apply_lock(int fd, int fcntltype, BOOL dofcntl, int fcntltime, BOOL doflock,
+ int flocktime)
+{
+int yield = 0;
+int save_errno;
+struct flock lock_data;
+lock_data.l_type = fcntltype;
+lock_data.l_whence = lock_data.l_start = lock_data.l_len = 0;
+
+sigalrm_seen = FALSE;
+
+if (dofcntl)
+ {
+ if (fcntltime > 0)
+ {
+ ALARM(fcntltime);
+ yield = fcntl(fd, F_SETLKW, &lock_data);
+ save_errno = errno;
+ ALARM_CLR(0);
+ errno = save_errno;
+ }
+ else yield = fcntl(fd, F_SETLK, &lock_data);
+ }
+
+#ifndef NO_FLOCK
+if (doflock && (yield >= 0))
+ {
+ int flocktype = (fcntltype == F_WRLCK) ? LOCK_EX : LOCK_SH;
+ if (flocktime > 0)
+ {
+ ALARM(flocktime);
+ yield = flock(fd, flocktype);
+ save_errno = errno;
+ ALARM_CLR(0);
+ errno = save_errno;
+ }
+ else yield = flock(fd, flocktype | LOCK_NB);
+ }
+#endif /* NO_FLOCK */
+
+return yield;
+}
+
+
+
+
+#ifdef SUPPORT_MBX
+/*************************************************
+* Copy message into MBX mailbox *
+*************************************************/
+
+/* This function is called when a message intended for a MBX mailbox has been
+written to a temporary file. We can now get the size of the message and then
+copy it in MBX format to the mailbox.
+
+Arguments:
+ to_fd fd to write to (the real mailbox)
+ from_fd fd to read from (the temporary file)
+ saved_size current size of mailbox
+
+Returns: OK if all went well, DEFER otherwise, with errno preserved
+ the number of bytes written are added to transport_count
+ by virtue of calling transport_write_block()
+*/
+
+/* Values taken from c-client */
+
+#define MBX_HDRSIZE 2048
+#define MBX_NUSERFLAGS 30
+
+static int
+copy_mbx_message(int to_fd, int from_fd, off_t saved_size)
+{
+int used;
+off_t size;
+struct stat statbuf;
+transport_ctx tctx = { .u={.fd = to_fd}, .options = topt_not_socket };
+
+/* If the current mailbox size is zero, write a header block */
+
+if (saved_size == 0)
+ {
+ uschar *s;
+ memset (deliver_out_buffer, '\0', MBX_HDRSIZE);
+ sprintf(CS(s = deliver_out_buffer), "*mbx*\015\012%08lx00000000\015\012",
+ (long int)time(NULL));
+ for (int i = 0; i < MBX_NUSERFLAGS; i++)
+ sprintf (CS(s += Ustrlen(s)), "\015\012");
+ if (!transport_write_block (&tctx, deliver_out_buffer, MBX_HDRSIZE, FALSE))
+ return DEFER;
+ }
+
+DEBUG(D_transport) debug_printf("copying MBX message from temporary file\n");
+
+/* Now construct the message's header from the time and the RFC822 file
+size, including CRLFs, which is the size of the input (temporary) file. */
+
+if (fstat(from_fd, &statbuf) < 0) return DEFER;
+size = statbuf.st_size;
+
+sprintf (CS deliver_out_buffer, "%s," OFF_T_FMT ";%08lx%04x-%08x\015\012",
+ tod_stamp(tod_mbx), size, 0L, 0, 0);
+used = Ustrlen(deliver_out_buffer);
+
+/* Rewind the temporary file, and copy it over in chunks. */
+
+if (lseek(from_fd, 0 , SEEK_SET) < 0) return DEFER;
+
+while (size > 0)
+ {
+ int len = read(from_fd, deliver_out_buffer + used,
+ DELIVER_OUT_BUFFER_SIZE - used);
+ if (len <= 0)
+ {
+ if (len == 0) errno = ERRNO_MBXLENGTH;
+ return DEFER;
+ }
+ if (!transport_write_block(&tctx, deliver_out_buffer, used + len, FALSE))
+ return DEFER;
+ size -= len;
+ used = 0;
+ }
+
+return OK;
+}
+#endif /* SUPPORT_MBX */
+
+
+
+/*************************************************
+* Check creation is permitted *
+*************************************************/
+
+/* This function checks whether a given file name is permitted to be created,
+as controlled by the create_file option. If no home directory is set, however,
+we can't do any tests.
+
+Arguments:
+ filename the file name
+ create_file the ob->create_file option
+ deliver_dir the delivery directory
+
+Returns: TRUE if creation is permitted
+*/
+
+static BOOL
+check_creation(uschar *filename, int create_file, const uschar * deliver_dir)
+{
+BOOL yield = TRUE;
+
+if (deliver_dir && create_file != create_anywhere)
+ {
+ int len = Ustrlen(deliver_dir);
+ uschar *file = filename;
+
+ while (file[0] == '/' && file[1] == '/') file++;
+ if ( Ustrncmp(file, deliver_dir, len) != 0
+ || file[len] != '/'
+ || Ustrchr(file+len+2, '/') != NULL
+ && ( create_file != create_belowhome
+ || Ustrstr(file+len, "/../") != NULL
+ )
+ ) yield = FALSE;
+
+ /* If yield is TRUE, the file name starts with the home directory, and does
+ not contain any instances of "/../" in the "belowhome" case. However, it may
+ still contain symbolic links. We can check for this by making use of
+ realpath(), which most Unixes seem to have (but make it possible to cut this
+ out). We can't just use realpath() on the whole file name, because we know
+ the file itself doesn't exist, and intermediate directories may also not
+ exist. What we want to know is the real path of the longest existing part of
+ the path. That must match the home directory's beginning, whichever is the
+ shorter. */
+
+ #ifndef NO_REALPATH
+ if (yield && create_file == create_belowhome)
+ {
+ uschar *next;
+ uschar *rp = NULL;
+ for (uschar * slash = Ustrrchr(file, '/'); /* There is known to be one */
+ !rp && slash > file; /* Stop if reached beginning */
+ slash = next)
+ {
+ *slash = 0;
+ rp = US realpath(CS file, CS big_buffer);
+ next = Ustrrchr(file, '/');
+ *slash = '/';
+ }
+
+ /* If rp == NULL it means that none of the relevant directories exist.
+ This is not a problem here - it means that no symbolic links can exist,
+ which is all we are worried about. Otherwise, we must compare it
+ against the start of the home directory. However, that may itself
+ contain symbolic links, so we have to "realpath" it as well, if
+ possible. */
+
+ if (rp)
+ {
+ uschar hdbuffer[PATH_MAX+1];
+ const uschar * rph = deliver_dir;
+ int rlen = Ustrlen(big_buffer);
+
+ if ((rp = US realpath(CS deliver_dir, CS hdbuffer)))
+ {
+ rph = hdbuffer;
+ len = Ustrlen(rph);
+ }
+
+ if (rlen > len) rlen = len;
+ if (Ustrncmp(rph, big_buffer, rlen) != 0)
+ {
+ yield = FALSE;
+ DEBUG(D_transport) debug_printf("Real path \"%s\" does not match \"%s\"\n",
+ big_buffer, deliver_dir);
+ }
+ }
+ }
+ #endif /* NO_REALPATH */
+ }
+
+return yield;
+}
+
+
+
+/*************************************************
+* Main entry point *
+*************************************************/
+
+/* See local README for general interface details. This transport always
+returns FALSE, indicating that the status which has been placed in the first
+address should be copied to any other addresses in a batch.
+
+Appendfile delivery is tricky and has led to various security problems in other
+mailers. The logic used here is therefore laid out in some detail. When this
+function is called, we are running in a subprocess which has had its gid and
+uid set to the appropriate values. Therefore, we cannot write directly to the
+exim logs. Any errors must be handled by setting appropriate return codes.
+Note that the default setting for addr->transport_return is DEFER, so it need
+not be set unless some other value is required.
+
+The code below calls geteuid() rather than getuid() to get the current uid
+because in weird configurations not running setuid root there may be a
+difference. In the standard configuration, where setuid() has been used in the
+delivery process, there will be no difference between the uid and the euid.
+
+(1) If the af_file flag is set, this is a delivery to a file after .forward or
+ alias expansion. Otherwise, there must be a configured file name or
+ directory name.
+
+The following items apply in the case when a file name (as opposed to a
+directory name) is given, that is, when appending to a single file:
+
+(2f) Expand the file name.
+
+(3f) If the file name is /dev/null, return success (optimization).
+
+(4f) If the file_format options is set, open the file for reading, and check
+ that the bytes at the start of the file match one of the given strings.
+ If the check indicates a transport other than the current one should be
+ used, pass control to that other transport. Otherwise continue. An empty
+ or non-existent file matches the current transport. The file is closed
+ after the check.
+
+(5f) If a lock file is required, create it (see extensive separate comments
+ below about the algorithm for doing this). It is important to do this
+ before opening the mailbox if NFS is in use.
+
+(6f) Stat the file, using lstat() rather than stat(), in order to pick up
+ details of any symbolic link.
+
+(7f) If the file already exists:
+
+ Check the owner and group if necessary, and defer if they are wrong.
+
+ If it is a symbolic link AND the allow_symlink option is set (NOT the
+ default), go back to (6f) but this time use stat() instead of lstat().
+
+ If it's not a regular file (or FIFO when permitted), defer delivery.
+
+ Check permissions. If the required permissions are *less* than the
+ existing ones, or supplied by the address (often by the user via filter),
+ chmod() the file. Otherwise, defer.
+
+ Save the inode number.
+
+ Open with O_RDRW + O_APPEND, thus failing if the file has vanished.
+
+ If open fails because the file does not exist, go to (6f); on any other
+ failure, defer.
+
+ Check the inode number hasn't changed - I realize this isn't perfect (an
+ inode can be reused) but it's cheap and will catch some of the races.
+
+ Check it's still a regular file (or FIFO if permitted).
+
+ Check that the owner and permissions haven't changed.
+
+ If file_format is set, check that the file still matches the format for
+ the current transport. If not, defer delivery.
+
+(8f) If file does not exist initially:
+
+ Open with O_WRONLY + O_EXCL + O_CREAT with configured mode, unless we know
+ this is via a symbolic link (only possible if allow_symlinks is set), in
+ which case don't use O_EXCL, as it doesn't work.
+
+ If open fails because the file already exists, go to (6f). To avoid
+ looping for ever in a situation where the file is continuously being
+ created and deleted, all of this happens inside a loop that operates
+ lock_retries times and includes the fcntl and flock locking. If the
+ loop completes without the file getting opened, defer and request
+ freezing, because something really weird is happening.
+
+ If open fails for any other reason, defer for subsequent delivery except
+ when this is a file delivery resulting from an alias or forward expansion
+ and the error is EPERM or ENOENT or EACCES, in which case FAIL as this is
+ most likely a user rather than a configuration error.
+
+(9f) We now have the file checked and open for writing. If so configured, lock
+ it using fcntl, flock, or MBX locking rules. If this fails, close the file
+ and goto (6f), up to lock_retries times, after sleeping for a while. If it
+ still fails, give up and defer delivery.
+
+(10f)Save the access time (for subsequent restoration) and the size of the
+ file, for comsat and for re-setting if delivery fails in the middle -
+ e.g. for quota exceeded.
+
+The following items apply in the case when a directory name is given:
+
+(2d) Create a new file in the directory using a temporary name, by opening for
+ writing and with O_CREAT. If maildir format is being used, the file
+ is created in a temporary subdirectory with a prescribed name. If
+ mailstore format is being used, the envelope file is first created with a
+ temporary name, then the data file.
+
+The following items apply in all cases:
+
+(11) We now have the file open for writing, and locked if it was given as a
+ file name. Write the message and flush the file, unless there is a setting
+ of the local quota option, in which case we can check for its excession
+ without doing any writing.
+
+ In the case of MBX format mailboxes, the message is first written to a
+ temporary file, in order to get its correct length. This is then copied to
+ the real file, preceded by an MBX header.
+
+ If there is a quota error on writing, defer the address. Timeout logic
+ will determine for how long retries are attempted. We restore the mailbox
+ to its original length if it's a single file. There doesn't seem to be a
+ uniform error code for quota excession (it even differs between SunOS4
+ and some versions of SunOS5) so a system-dependent macro called
+ ERRNO_QUOTA is used for it, and the value gets put into errno_quota at
+ compile time.
+
+ For any other error (most commonly disk full), do the same.
+
+The following applies after appending to a file:
+
+(12f)Restore the atime; notify_comsat if required; close the file (which
+ unlocks it if it was locked). Delete the lock file if it exists.
+
+The following applies after writing a unique file in a directory:
+
+(12d)For maildir format, rename the file into the new directory. For mailstore
+ format, rename the envelope file to its correct name. Otherwise, generate
+ a unique name from the directory_file option, and rename to that, possibly
+ trying a few times if the file exists and re-expanding the name gives a
+ different string.
+
+This transport yields FAIL only when a file name is generated by an alias or
+forwarding operation and attempting to open it gives EPERM, ENOENT, or EACCES.
+All other failures return DEFER (in addr->transport_return). */
+
+
+BOOL
+appendfile_transport_entry(
+ transport_instance *tblock, /* data for this instantiation */
+ address_item *addr) /* address we are working on */
+{
+appendfile_transport_options_block *ob =
+ (appendfile_transport_options_block *)(tblock->options_block);
+struct stat statbuf;
+const uschar * deliver_dir;
+uschar *fdname = NULL;
+uschar *filename = NULL;
+uschar *hitchname = NULL;
+uschar *dataname = NULL;
+uschar *lockname = NULL;
+uschar *newname = NULL;
+uschar *nametag = NULL;
+uschar *cr = US"";
+uschar *filecount_msg = US"";
+uschar *path;
+struct utimbuf times;
+struct timeval msg_tv;
+BOOL disable_quota = FALSE;
+BOOL isdirectory = FALSE;
+BOOL isfifo = FALSE;
+BOOL wait_for_tick = FALSE;
+uid_t uid = geteuid(); /* See note above */
+gid_t gid = getegid();
+int mbformat;
+int mode = (addr->mode > 0) ? addr->mode : ob->mode;
+off_t saved_size = -1;
+off_t mailbox_size = ob->mailbox_size_value;
+int mailbox_filecount = ob->mailbox_filecount_value;
+int hd = -1;
+int fd = -1;
+int yield = FAIL;
+int i;
+
+#ifdef SUPPORT_MBX
+int save_fd = 0;
+int mbx_lockfd = -1;
+uschar mbx_lockname[40];
+FILE *temp_file = NULL;
+#endif /* SUPPORT_MBX */
+
+#ifdef SUPPORT_MAILDIR
+int maildirsize_fd = -1; /* fd for maildirsize file */
+int maildir_save_errno;
+#endif
+
+
+DEBUG(D_transport) debug_printf("appendfile transport entered\n");
+
+/* An "address_file" or "address_directory" transport is used to deliver to
+files specified via .forward or an alias file. Prior to release 4.20, the
+"file" and "directory" options were ignored in this case. This has been changed
+to allow the redirection data to specify what is in effect a folder, whose
+location is determined by the options on the transport.
+
+Compatibility with the case when neither option is set is retained by forcing a
+value for the file or directory name. A directory delivery is assumed if the
+last character of the path from the router is '/'.
+
+The file path is in the local part of the address, but not in the $local_part
+variable (that holds the parent local part). It is, however, in the
+$address_file variable. Below, we update the local part in the address if it
+changes by expansion, so that the final path ends up in the log. */
+
+if (testflag(addr, af_file) && !ob->filename && !ob->dirname)
+ {
+ fdname = US"$address_file";
+ if (address_file[Ustrlen(address_file)-1] == '/' ||
+ ob->maildir_format ||
+ ob->mailstore_format)
+ isdirectory = TRUE;
+ }
+
+/* Handle (a) an "address file" delivery where "file" or "directory" is
+explicitly set and (b) a non-address_file delivery, where one of "file" or
+"directory" must be set; initialization ensures that they are not both set. */
+
+if (!fdname)
+ {
+ if (!(fdname = ob->filename))
+ {
+ fdname = ob->dirname;
+ isdirectory = TRUE;
+ }
+ if (!fdname)
+ {
+ addr->message = string_sprintf("Mandatory file or directory option "
+ "missing from %s transport", tblock->name);
+ goto ret_panic;
+ }
+ }
+
+/* Maildir and mailstore require a directory */
+
+if ((ob->maildir_format || ob->mailstore_format) && !isdirectory)
+ {
+ addr->message = string_sprintf("mail%s_format requires \"directory\" "
+ "to be specified for the %s transport",
+ ob->maildir_format ? "dir" : "store", tblock->name);
+ goto ret_panic;
+ }
+
+if (!(path = expand_string(fdname)))
+ {
+ addr->message = string_sprintf("Expansion of \"%s\" (file or directory "
+ "name for %s transport) failed: %s", fdname, tblock->name,
+ expand_string_message);
+ goto ret_panic;
+ }
+
+if (path[0] != '/')
+ {
+ addr->message = string_sprintf("appendfile: file or directory name "
+ "\"%s\" is not absolute", path);
+ addr->basic_errno = ERRNO_NOTABSOLUTE;
+ return FALSE;
+ }
+
+/* For a file delivery, make sure the local part in the address(es) is updated
+to the true local part. */
+
+if (testflag(addr, af_file))
+ for (address_item * addr2 = addr; addr2; addr2 = addr2->next)
+ addr2->local_part = string_copy(path);
+
+/* The available mailbox formats depend on whether it is a directory or a file
+delivery. */
+
+if (isdirectory)
+ {
+ mbformat =
+ #ifdef SUPPORT_MAILDIR
+ ob->maildir_format ? mbf_maildir :
+ #endif
+ #ifdef SUPPORT_MAILSTORE
+ ob->mailstore_format ? mbf_mailstore :
+ #endif
+ mbf_smail;
+ }
+else
+ {
+ mbformat =
+ #ifdef SUPPORT_MBX
+ ob->mbx_format ? mbf_mbx :
+ #endif
+ mbf_unix;
+ }
+
+DEBUG(D_transport)
+ {
+ debug_printf("appendfile: mode=%o notify_comsat=%d quota=" OFF_T_FMT
+ "%s%s"
+ " warning=" OFF_T_FMT "%s\n"
+ " %s=%s format=%s\n message_prefix=%s\n message_suffix=%s\n "
+ "maildir_use_size_file=%s\n",
+ mode, ob->notify_comsat, ob->quota_value,
+ ob->quota_no_check ? " (no_check)" : "",
+ ob->quota_filecount_no_check ? " (no_check_filecount)" : "",
+ ob->quota_warn_threshold_value,
+ ob->quota_warn_threshold_is_percent ? "%" : "",
+ isdirectory ? "directory" : "file",
+ path, mailbox_formats[mbformat],
+ !ob->message_prefix ? US"null" : string_printing(ob->message_prefix),
+ !ob->message_suffix ? US"null" : string_printing(ob->message_suffix),
+ ob->maildir_use_size_file ? "yes" : "no");
+
+ if (!isdirectory) debug_printf(" locking by %s%s%s%s%s\n",
+ ob->use_lockfile ? "lockfile " : "",
+ ob->use_mbx_lock ? "mbx locking (" : "",
+ ob->use_fcntl ? "fcntl " : "",
+ ob->use_flock ? "flock" : "",
+ ob->use_mbx_lock ? ")" : "");
+ }
+
+/* If the -N option is set, can't do any more. */
+
+if (f.dont_deliver)
+ {
+ DEBUG(D_transport)
+ debug_printf("*** delivery by %s transport bypassed by -N option\n",
+ tblock->name);
+ addr->transport_return = OK;
+ return FALSE;
+ }
+
+/* If an absolute path was given for create_file the it overrides deliver_home
+(here) and de-taints the filename (below, after check_creation() */
+
+deliver_dir = *ob->create_file_string == '/'
+ ? ob->create_file_string : deliver_home;
+
+/* Handle the case of a file name. If the file name is /dev/null, we can save
+ourselves some effort and just give a success return right away. */
+
+if (!isdirectory)
+ {
+ BOOL use_lstat = TRUE;
+ BOOL file_opened = FALSE;
+ BOOL allow_creation_here = TRUE;
+
+ if (Ustrcmp(path, "/dev/null") == 0)
+ {
+ addr->transport_return = OK;
+ return FALSE;
+ }
+
+ /* Set the name of the file to be opened, and the file to which the data
+ is written, and find out if we are permitted to create a non-existent file.
+ If the create_file option is an absolute path and the file was within it,
+ de-taint. Chaeck for a tainted path. */
+
+ if ( (allow_creation_here = check_creation(path, ob->create_file, deliver_dir))
+ && ob->create_file == create_belowhome)
+ if (is_tainted(path))
+ {
+ DEBUG(D_transport) debug_printf("de-tainting path '%s'\n", path);
+ path = string_copy_taint(path, GET_UNTAINTED);
+ }
+
+ if (is_tainted(path)) goto tainted_ret_panic;
+ dataname = filename = path;
+
+ /* If ob->create_directory is set, attempt to create the directories in
+ which this mailbox lives, but only if we are permitted to create the file
+ itself. We know we are dealing with an absolute path, because this was
+ checked above. */
+
+ if (ob->create_directory && allow_creation_here)
+ {
+ uschar *p = Ustrrchr(path, '/');
+ p = string_copyn(path, p - path);
+ if (!directory_make(NULL, p, ob->dirmode, FALSE))
+ {
+ addr->basic_errno = errno;
+ addr->message =
+ string_sprintf("failed to create directories for %s: %s", path,
+ exim_errstr(errno));
+ DEBUG(D_transport) debug_printf("%s transport: %s\n", tblock->name, path);
+ return FALSE;
+ }
+ }
+
+ /* If file_format is set we must check that any existing file matches one of
+ the configured formats by checking the bytes it starts with. A match then
+ indicates a specific transport - if it is not this one, pass control to it.
+ Otherwise carry on here. An empty or non-existent file matches the current
+ transport. We don't need to distinguish between non-existence and other open
+ failures because if an existing file fails to open here, it will also fail
+ again later when O_RDWR is used. */
+
+ if (ob->file_format)
+ {
+ int cfd = Uopen(path, O_RDONLY, 0);
+ if (cfd >= 0)
+ {
+ transport_instance *tt = check_file_format(cfd, tblock, addr);
+ (void)close(cfd);
+
+ /* If another transport is indicated, call it and return; if no transport
+ was found, just return - the error data will have been set up.*/
+
+ if (tt != tblock)
+ {
+ if (tt)
+ {
+ set_process_info("delivering %s to %s using %s", message_id,
+ addr->local_part, tt->name);
+ debug_print_string(tt->debug_string);
+ addr->transport = tt;
+ (tt->info->code)(tt, addr);
+ }
+ return FALSE;
+ }
+ }
+ }
+
+ /* The locking of mailbox files is worse than the naming of cats, which is
+ known to be "a difficult matter" (T.S. Eliot) and just as cats must have
+ three different names, so several different styles of locking are used.
+
+ Research in other programs that lock mailboxes shows that there is no
+ universally standard method. Having mailboxes NFS-mounted on the system that
+ is delivering mail is not the best thing, but people do run like this,
+ and so the code must do its best to cope.
+
+ Three different locking mechanisms are supported. The initialization function
+ checks that at least one is configured.
+
+ LOCK FILES
+
+ Unless no_use_lockfile is set, we attempt to build a lock file in a way that
+ will work over NFS. Only after that is done do we actually open the mailbox
+ and apply locks to it (if configured).
+
+ Originally, Exim got the file opened before doing anything about locking.
+ However, a very occasional problem was observed on Solaris 2 when delivering
+ over NFS. It is seems that when a file is opened with O_APPEND, the file size
+ gets remembered at open time. If another process on another host (that's
+ important) has the file open and locked and writes to it and then releases
+ the lock while the first process is waiting to get the lock, the first
+ process may fail to write at the new end point of the file - despite the very
+ definite statement about O_APPEND in the man page for write(). Experiments
+ have reproduced this problem, but I do not know any way of forcing a host to
+ update its attribute cache for an open NFS file. It would be nice if it did
+ so when a lock was taken out, but this does not seem to happen. Anyway, to
+ reduce the risk of this problem happening, we now create the lock file
+ (if configured) *before* opening the mailbox. That will prevent two different
+ Exims opening the file simultaneously. It may not prevent clashes with MUAs,
+ however, but Pine at least seems to operate in the same way.
+
+ Lockfiles should normally be used when NFS is involved, because of the above
+ problem.
+
+ The logic for creating the lock file is:
+
+ . The name of the lock file is <mailbox-name>.lock
+
+ . First, create a "hitching post" name by adding the primary host name,
+ current time and pid to the lock file name. This should be unique.
+
+ . Create the hitching post file using WRONLY + CREAT + EXCL.
+
+ . If that fails EACCES, we assume it means that the user is unable to create
+ files in the mail spool directory. Some installations might operate in this
+ manner, so there is a configuration option to allow this state not to be an
+ error - we proceed to lock using fcntl only, after the file is open.
+
+ . Otherwise, an error causes a deferment of the address.
+
+ . Hard link the hitching post to the lock file name.
+
+ . If the link succeeds, we have successfully created the lock file. Simply
+ close and unlink the hitching post file.
+
+ . If the link does not succeed, proceed as follows:
+
+ o Fstat the hitching post file, and then close and unlink it.
+
+ o Now examine the stat data. If the number of links to the file is exactly
+ 2, the linking succeeded but for some reason, e.g. an NFS server crash,
+ the return never made it back, so the link() function gave a failure
+ return.
+
+ . This method allows for the lock file to be created by some other process
+ right up to the moment of the attempt to hard link it, and is also robust
+ against NFS server crash-reboots, which would probably result in timeouts
+ in the middle of link().
+
+ . System crashes may cause lock files to get left lying around, and some means
+ of flushing them is required. The approach of writing a pid (used by smail
+ and by elm) into the file isn't useful when NFS may be in use. Pine uses a
+ timeout, which seems a better approach. Since any program that writes to a
+ mailbox using a lock file should complete its task very quickly, Pine
+ removes lock files that are older than 5 minutes. We allow the value to be
+ configurable on the transport.
+
+ FCNTL LOCKING
+
+ If use_fcntl_lock is set, then Exim gets an exclusive fcntl() lock on the
+ mailbox once it is open. This is done by default with a non-blocking lock.
+ Failures to lock cause retries after a sleep, but only for a certain number
+ of tries. A blocking lock is deliberately not used so that we don't hold the
+ mailbox open. This minimizes the possibility of the NFS problem described
+ under LOCK FILES above, if for some reason NFS deliveries are happening
+ without lock files. However, the use of a non-blocking lock and sleep, though
+ the safest approach, does not give the best performance on very busy systems.
+ A blocking lock plus timeout does better. Therefore Exim has an option to
+ allow it to work this way. If lock_fcntl_timeout is set greater than zero, it
+ enables the use of blocking fcntl() calls.
+
+ FLOCK LOCKING
+
+ If use_flock_lock is set, then Exim gets an exclusive flock() lock in the
+ same manner as for fcntl locking above. No-blocking/timeout is also set as
+ above in lock_flock_timeout. Not all operating systems provide or support
+ flock(). For those that don't (as determined by the definition of LOCK_SH in
+ /usr/include/sys/file.h), use_flock_lock may not be set. For some OS, flock()
+ is implemented (not precisely) on top of fcntl(), which means there's no
+ point in actually using it.
+
+ MBX LOCKING
+
+ If use_mbx_lock is set (this is supported only if SUPPORT_MBX is defined)
+ then the rules used for locking in c-client are used. Exim takes out a shared
+ lock on the mailbox file, and an exclusive lock on the file whose name is
+ /tmp/.<device-number>.<inode-number>. The shared lock on the mailbox stops
+ any other MBX client from getting an exclusive lock on it and expunging it.
+ It also stops any other MBX client from unlinking the /tmp lock when it has
+ finished with it.
+
+ The exclusive lock on the /tmp file prevents any other MBX client from
+ updating the mailbox in any way. When writing is finished, if an exclusive
+ lock on the mailbox can be obtained, indicating there are no current sharers,
+ the /tmp file is unlinked.
+
+ MBX locking can use either fcntl() or flock() locking. If neither
+ use_fcntl_lock or use_flock_lock is set, it defaults to using fcntl() only.
+ The calls for getting these locks are by default non-blocking, as for non-mbx
+ locking, but can be made blocking by setting lock_fcntl_timeout and/or
+ lock_flock_timeout as appropriate. As MBX delivery doesn't work over NFS, it
+ probably makes sense to set timeouts for any MBX deliveries. */
+
+
+ /* Build a lock file if configured to do so - the existence of a lock
+ file is subsequently checked by looking for a non-negative value of the
+ file descriptor hd - even though the file is no longer open. */
+
+ if (ob->use_lockfile)
+ {
+ /* cf. exim_lock.c */
+ lockname = string_sprintf("%s.lock", filename);
+ hitchname = string_sprintf( "%s.%s.%08x.%08x", lockname, primary_hostname,
+ (unsigned int)(time(NULL)), (unsigned int)getpid());
+
+ DEBUG(D_transport) debug_printf("lock name: %s\nhitch name: %s\n", lockname,
+ hitchname);
+
+ /* Lock file creation retry loop */
+
+ for (i = 0; i < ob->lock_retries; sleep(ob->lock_interval), i++)
+ {
+ int rc;
+
+ hd = Uopen(hitchname, O_WRONLY | O_CREAT | O_EXCL, ob->lockfile_mode);
+ if (hd < 0)
+ {
+ addr->basic_errno = errno;
+ addr->message =
+ string_sprintf("creating lock file hitching post %s "
+ "(euid=%ld egid=%ld)", hitchname, (long int)geteuid(),
+ (long int)getegid());
+ return FALSE;
+ }
+
+ /* Attempt to hitch the hitching post to the lock file. If link()
+ succeeds (the common case, we hope) all is well. Otherwise, fstat the
+ file, and get rid of the hitching post. If the number of links was 2,
+ the link was created, despite the failure of link(). If the hitch was
+ not successful, try again, having unlinked the lock file if it is too
+ old.
+
+ There's a version of Linux (2.0.27) which doesn't update its local cache
+ of the inode after link() by default - which many think is a bug - but
+ if the link succeeds, this code will be OK. It just won't work in the
+ case when link() fails after having actually created the link. The Linux
+ NFS person is fixing this; a temporary patch is available if anyone is
+ sufficiently worried. */
+
+ if ((rc = Ulink(hitchname, lockname)) != 0) fstat(hd, &statbuf);
+ (void)close(hd);
+ Uunlink(hitchname);
+ if (rc != 0 && statbuf.st_nlink != 2)
+ {
+ if (ob->lockfile_timeout > 0 && Ustat(lockname, &statbuf) == 0 &&
+ time(NULL) - statbuf.st_ctime > ob->lockfile_timeout)
+ {
+ DEBUG(D_transport) debug_printf("unlinking timed-out lock file\n");
+ Uunlink(lockname);
+ }
+ DEBUG(D_transport) debug_printf("link of hitching post failed - retrying\n");
+ continue;
+ }
+
+ DEBUG(D_transport) debug_printf("lock file created\n");
+ break;
+ }
+
+ /* Check for too many tries at creating the lock file */
+
+ if (i >= ob->lock_retries)
+ {
+ addr->basic_errno = ERRNO_LOCKFAILED;
+ addr->message = string_sprintf("failed to lock mailbox %s (lock file)",
+ filename);
+ return FALSE;
+ }
+ }
+
+
+ /* We now have to get the file open. First, stat() it and act on existence or
+ non-existence. This is in a loop to handle the case of a file's being created
+ or deleted as we watch, and also to handle retries when the locking fails.
+ Rather than holding the file open while waiting for the fcntl() and/or
+ flock() lock, we close and do the whole thing again. This should be safer,
+ especially for NFS files, which might get altered from other hosts, making
+ their cached sizes incorrect.
+
+ With the default settings, no symlinks are permitted, but there is an option
+ to permit symlinks for those sysadmins that know what they are doing.
+ Shudder. However, insist that the initial symlink is owned by the right user.
+ Thus lstat() is used initially; if a symlink is discovered, the loop is
+ repeated such that stat() is used, to look at the end file. */
+
+ for (i = 0; i < ob->lock_retries; i++)
+ {
+ int sleep_before_retry = TRUE;
+ file_opened = FALSE;
+
+ if ((use_lstat ? Ulstat(filename, &statbuf) : Ustat(filename, &statbuf)) != 0)
+ {
+ /* Let's hope that failure to stat (other than non-existence) is a
+ rare event. */
+
+ if (errno != ENOENT)
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("attempting to stat mailbox %s",
+ filename);
+ goto RETURN;
+ }
+
+ /* File does not exist. If it is required to pre-exist this state is an
+ error. */
+
+ if (ob->file_must_exist)
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("mailbox %s does not exist, "
+ "but file_must_exist is set", filename);
+ goto RETURN;
+ }
+
+ /* If not permitted to create this file because it isn't in or below
+ the home directory, generate an error. */
+
+ if (!allow_creation_here)
+ {
+ addr->basic_errno = ERRNO_BADCREATE;
+ addr->message = string_sprintf("mailbox %s does not exist, "
+ "but creation outside the home directory is not permitted",
+ filename);
+ goto RETURN;
+ }
+
+ /* Attempt to create and open the file. If open fails because of
+ pre-existence, go round the loop again. For any other error, defer the
+ address, except for an alias or forward generated file name with EPERM,
+ ENOENT, or EACCES, as those are most likely to be user errors rather
+ than Exim config errors. When a symbolic link is permitted and points
+ to a non-existent file, we get here with use_lstat = FALSE. In this case
+ we mustn't use O_EXCL, since it doesn't work. The file is opened RDRW for
+ consistency and because MBX locking requires it in order to be able to
+ get a shared lock. */
+
+ fd = Uopen(filename, O_RDWR | O_APPEND | O_CREAT |
+ (use_lstat ? O_EXCL : 0), mode);
+ if (fd < 0)
+ {
+ if (errno == EEXIST) continue;
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while creating mailbox %s",
+ filename);
+ if (testflag(addr, af_file) &&
+ (errno == EPERM || errno == ENOENT || errno == EACCES))
+ addr->transport_return = FAIL;
+ goto RETURN;
+ }
+
+ /* We have successfully created and opened the file. Ensure that the group
+ and the mode are correct. */
+
+ if (exim_chown(filename, uid, gid) || Uchmod(filename, mode))
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while setting perms on mailbox %s",
+ filename);
+ addr->transport_return = FAIL;
+ goto RETURN;
+ }
+ }
+
+
+ /* The file already exists. Test its type, ownership, and permissions, and
+ save the inode for checking later. If symlinks are permitted (not the
+ default or recommended state) it may be a symlink that already exists.
+ Check its ownership and then look for the file at the end of the link(s).
+ This at least prevents one user creating a symlink for another user in
+ a sticky directory. */
+
+ else
+ {
+ int oldmode = (int)statbuf.st_mode;
+ ino_t inode = statbuf.st_ino;
+ BOOL islink = (oldmode & S_IFMT) == S_IFLNK;
+
+ isfifo = FALSE; /* In case things are changing */
+
+ /* Check owner if required - the default. */
+
+ if (ob->check_owner && statbuf.st_uid != uid)
+ {
+ addr->basic_errno = ERRNO_BADUGID;
+ addr->message = string_sprintf("mailbox %s%s has wrong uid "
+ "(%ld != %ld)", filename,
+ islink ? " (symlink)" : "",
+ (long int)(statbuf.st_uid), (long int)uid);
+ goto RETURN;
+ }
+
+ /* Group is checked only if check_group is set. */
+
+ if (ob->check_group && statbuf.st_gid != gid)
+ {
+ addr->basic_errno = ERRNO_BADUGID;
+ addr->message = string_sprintf("mailbox %s%s has wrong gid (%d != %d)",
+ filename, islink ? " (symlink)" : "", statbuf.st_gid, gid);
+ goto RETURN;
+ }
+
+ /* Just in case this is a sticky-bit mail directory, we don't want
+ users to be able to create hard links to other users' files. */
+
+ if (statbuf.st_nlink != 1)
+ {
+ addr->basic_errno = ERRNO_NOTREGULAR;
+ addr->message = string_sprintf("mailbox %s%s has too many links (%lu)",
+ filename, islink ? " (symlink)" : "", (unsigned long)statbuf.st_nlink);
+ goto RETURN;
+
+ }
+
+ /* If symlinks are permitted (not recommended), the lstat() above will
+ have found the symlink. Its ownership has just been checked; go round
+ the loop again, using stat() instead of lstat(). That will never yield a
+ mode of S_IFLNK. */
+
+ if (islink && ob->allow_symlink)
+ {
+ use_lstat = FALSE;
+ i--; /* Don't count this time round */
+ continue;
+ }
+
+ /* An actual file exists. Check that it is a regular file, or FIFO
+ if permitted. */
+
+ if (ob->allow_fifo && (oldmode & S_IFMT) == S_IFIFO) isfifo = TRUE;
+
+ else if ((oldmode & S_IFMT) != S_IFREG)
+ {
+ addr->basic_errno = ERRNO_NOTREGULAR;
+ addr->message = string_sprintf("mailbox %s is not a regular file%s",
+ filename, ob->allow_fifo ? " or named pipe" : "");
+ goto RETURN;
+ }
+
+ /* If the mode is not what it would be for a newly created file, change
+ the permissions if the mode is supplied for the address. Otherwise,
+ reduce but do not extend the permissions. If the newly created
+ permissions are greater than the existing permissions, don't change
+ things when the mode is not from the address. */
+
+ if ((oldmode &= 07777) != mode)
+ {
+ int diffs = oldmode ^ mode;
+ if (addr->mode > 0 || (diffs & oldmode) == diffs)
+ {
+ DEBUG(D_transport) debug_printf("chmod %o %s\n", mode, filename);
+ if (Uchmod(filename, mode) < 0)
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("attempting to chmod mailbox %s",
+ filename);
+ goto RETURN;
+ }
+ oldmode = mode;
+ }
+
+ /* Mode not from address, and newly-created permissions are greater
+ than existing permissions. Default is to complain, but it can be
+ configured to go ahead and try to deliver anyway if that's what
+ the administration wants. */
+
+ else if (ob->mode_fail_narrower)
+ {
+ addr->basic_errno = ERRNO_BADMODE;
+ addr->message = string_sprintf("mailbox %s has the wrong mode %o "
+ "(%o expected)", filename, oldmode, mode);
+ goto RETURN;
+ }
+ }
+
+ /* We are happy with the existing file. Open it, and then do further
+ tests to ensure that it is the same file that we were just looking at.
+ If the file does not now exist, restart this loop, going back to using
+ lstat again. For an NFS error, just defer; other opening errors are
+ more serious. The file is opened RDWR so that its format can be checked,
+ and also MBX locking requires the use of a shared (read) lock. However,
+ a FIFO is opened WRONLY + NDELAY so that it fails if there is no process
+ reading the pipe. */
+
+ fd = Uopen(filename, isfifo ? (O_WRONLY|O_NDELAY) : (O_RDWR|O_APPEND),
+ mode);
+ if (fd < 0)
+ {
+ if (errno == ENOENT)
+ {
+ use_lstat = TRUE;
+ continue;
+ }
+ addr->basic_errno = errno;
+ if (isfifo)
+ addr->message = string_sprintf("while opening named pipe %s "
+ "(could mean no process is reading it)", filename);
+ else if (errno != EWOULDBLOCK)
+ addr->message = string_sprintf("while opening mailbox %s", filename);
+ goto RETURN;
+ }
+
+ /* This fstat really shouldn't fail, as we have an open file! There's a
+ dilemma here. We use fstat in order to be sure we are peering at the file
+ we have got open. However, that won't tell us if the file was reached
+ via a symbolic link. We checked this above, but there is a race exposure
+ if the link was created between the previous lstat and the open. However,
+ it would have to be created with the same inode in order to pass the
+ check below. If ob->allow_symlink is set, causing the use of stat rather
+ than lstat above, symbolic links may be there anyway, and the checking is
+ weaker. */
+
+ if (fstat(fd, &statbuf) < 0)
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("attempting to stat open mailbox %s",
+ filename);
+ goto RETURN;
+ }
+
+ /* Check the inode; this is isn't a perfect check, but gives some
+ confidence. */
+
+ if (inode != statbuf.st_ino)
+ {
+ addr->basic_errno = ERRNO_INODECHANGED;
+ addr->message = string_sprintf("opened mailbox %s inode number changed "
+ "from " INO_T_FMT " to " INO_T_FMT, filename, inode, statbuf.st_ino);
+ addr->special_action = SPECIAL_FREEZE;
+ goto RETURN;
+ }
+
+ /* Check it's still a regular file or FIFO, and the uid, gid, and
+ permissions have not changed. */
+
+ if ((!isfifo && (statbuf.st_mode & S_IFMT) != S_IFREG) ||
+ (isfifo && (statbuf.st_mode & S_IFMT) != S_IFIFO))
+ {
+ addr->basic_errno = ERRNO_NOTREGULAR;
+ addr->message =
+ string_sprintf("opened mailbox %s is no longer a %s", filename,
+ isfifo ? "named pipe" : "regular file");
+ addr->special_action = SPECIAL_FREEZE;
+ goto RETURN;
+ }
+
+ if ((ob->check_owner && statbuf.st_uid != uid) ||
+ (ob->check_group && statbuf.st_gid != gid))
+ {
+ addr->basic_errno = ERRNO_BADUGID;
+ addr->message =
+ string_sprintf("opened mailbox %s has wrong uid or gid", filename);
+ addr->special_action = SPECIAL_FREEZE;
+ goto RETURN;
+ }
+
+ if ((statbuf.st_mode & 07777) != oldmode)
+ {
+ addr->basic_errno = ERRNO_BADMODE;
+ addr->message = string_sprintf("opened mailbox %s has wrong mode %o "
+ "(%o expected)", filename, statbuf.st_mode & 07777, mode);
+ addr->special_action = SPECIAL_FREEZE;
+ goto RETURN;
+ }
+
+ /* If file_format is set, check that the format of the file has not
+ changed. Error data is set by the testing function. */
+
+ if (ob->file_format && check_file_format(fd, tblock, addr) != tblock)
+ {
+ addr->message = US"open mailbox has changed format";
+ goto RETURN;
+ }
+
+ /* The file is OK. Carry on to do the locking. */
+ }
+
+ /* We now have an open file, and must lock it using fcntl(), flock() or MBX
+ locking rules if configured to do so. If a lock file is also required, it
+ was created above and hd was left >= 0. At least one form of locking is
+ required by the initialization function. If locking fails here, close the
+ file and go round the loop all over again, after waiting for a bit, unless
+ blocking locking was used. */
+
+ file_opened = TRUE;
+ if ((ob->lock_fcntl_timeout > 0) || (ob->lock_flock_timeout > 0))
+ sleep_before_retry = FALSE;
+
+ /* Simple fcntl() and/or flock() locking */
+
+ if (!ob->use_mbx_lock && (ob->use_fcntl || ob->use_flock))
+ {
+ if (apply_lock(fd, F_WRLCK, ob->use_fcntl, ob->lock_fcntl_timeout,
+ ob->use_flock, ob->lock_flock_timeout) >= 0) break;
+ }
+
+ /* MBX locking rules */
+
+ #ifdef SUPPORT_MBX
+ else if (ob->use_mbx_lock)
+ {
+ int mbx_tmp_oflags;
+ struct stat lstatbuf, statbuf2;
+ if (apply_lock(fd, F_RDLCK, ob->use_fcntl, ob->lock_fcntl_timeout,
+ ob->use_flock, ob->lock_flock_timeout) >= 0 &&
+ fstat(fd, &statbuf) >= 0)
+ {
+ sprintf(CS mbx_lockname, "/tmp/.%lx.%lx", (long)statbuf.st_dev,
+ (long)statbuf.st_ino);
+
+ /*
+ * 2010-05-29: SECURITY
+ * Dan Rosenberg reported the presence of a race-condition in the
+ * original code here. Beware that many systems still allow symlinks
+ * to be followed in /tmp so an attacker can create a symlink pointing
+ * elsewhere between a stat and an open, which we should avoid
+ * following.
+ *
+ * It's unfortunate that we can't just use all the heavily debugged
+ * locking from above.
+ *
+ * Also: remember to mirror changes into exim_lock.c */
+
+ /* first leave the old pre-check in place, it provides better
+ * diagnostics for common cases */
+ if (Ulstat(mbx_lockname, &statbuf) >= 0)
+ {
+ if ((statbuf.st_mode & S_IFMT) == S_IFLNK)
+ {
+ addr->basic_errno = ERRNO_LOCKFAILED;
+ addr->message = string_sprintf("symbolic link on MBX lock file %s",
+ mbx_lockname);
+ goto RETURN;
+ }
+ if (statbuf.st_nlink > 1)
+ {
+ addr->basic_errno = ERRNO_LOCKFAILED;
+ addr->message = string_sprintf("hard link to MBX lock file %s",
+ mbx_lockname);
+ goto RETURN;
+ }
+ }
+
+ /* If we could just declare "we must be the ones who create this
+ * file" then a hitching post in a subdir would work, since a
+ * subdir directly in /tmp/ which we create wouldn't follow links
+ * but this isn't our locking logic, so we can't safely change the
+ * file existence rules. */
+
+ /* On systems which support O_NOFOLLOW, it's the easiest and most
+ * obviously correct security fix */
+ mbx_tmp_oflags = O_RDWR | O_CREAT;
+#ifdef O_NOFOLLOW
+ mbx_tmp_oflags |= O_NOFOLLOW;
+#endif
+ mbx_lockfd = Uopen(mbx_lockname, mbx_tmp_oflags, ob->lockfile_mode);
+ if (mbx_lockfd < 0)
+ {
+ addr->basic_errno = ERRNO_LOCKFAILED;
+ addr->message = string_sprintf("failed to open MBX lock file %s :%s",
+ mbx_lockname, strerror(errno));
+ goto RETURN;
+ }
+
+ if (Ulstat(mbx_lockname, &lstatbuf) < 0)
+ {
+ addr->basic_errno = ERRNO_LOCKFAILED;
+ addr->message = string_sprintf("attempting to lstat open MBX "
+ "lock file %s: %s", mbx_lockname, strerror(errno));
+ goto RETURN;
+ }
+ if (fstat(mbx_lockfd, &statbuf2) < 0)
+ {
+ addr->basic_errno = ERRNO_LOCKFAILED;
+ addr->message = string_sprintf("attempting to stat fd of open MBX "
+ "lock file %s: %s", mbx_lockname, strerror(errno));
+ goto RETURN;
+ }
+
+ /*
+ * At this point:
+ * statbuf: if exists, is file which existed prior to opening the
+ * lockfile, might have been replaced since then
+ * statbuf2: result of stat'ing the open fd, is what was actually
+ * opened
+ * lstatbuf: result of lstat'ing the filename immediately after
+ * the open but there's a race condition again between
+ * those two steps: before open, symlink to foo, after
+ * open but before lstat have one of:
+ * * was no symlink, so is the opened file
+ * (we created it, no messing possible after that point)
+ * * hardlink to foo
+ * * symlink elsewhere
+ * * hardlink elsewhere
+ * * new file/other
+ * Don't want to compare to device of /tmp because some modern systems
+ * have regressed to having /tmp be the safe actual filesystem as
+ * valuable data, so is mostly worthless, unless we assume that *only*
+ * Linux systems do this and that all Linux has O_NOFOLLOW. Something
+ * for further consideration.
+ * No point in doing a readlink on the lockfile as that will always be
+ * at a different point in time from when we open it, so tells us
+ * nothing; attempts to clean up and delete after ourselves would risk
+ * deleting a *third* filename.
+ */
+ if ((statbuf2.st_nlink > 1) ||
+ (lstatbuf.st_nlink > 1) ||
+ (!S_ISREG(lstatbuf.st_mode)) ||
+ (lstatbuf.st_dev != statbuf2.st_dev) ||
+ (lstatbuf.st_ino != statbuf2.st_ino))
+ {
+ addr->basic_errno = ERRNO_LOCKFAILED;
+ addr->message = string_sprintf("RACE CONDITION detected: "
+ "mismatch post-initial-checks between \"%s\" and opened "
+ "fd lead us to abort!", mbx_lockname);
+ goto RETURN;
+ }
+
+ (void)Uchmod(mbx_lockname, ob->lockfile_mode);
+
+ if (apply_lock(mbx_lockfd, F_WRLCK, ob->use_fcntl,
+ ob->lock_fcntl_timeout, ob->use_flock, ob->lock_flock_timeout) >= 0)
+ {
+ struct stat ostatbuf;
+
+ /* This tests for a specific race condition. Ensure that we still
+ have the same file. */
+
+ if (Ulstat(mbx_lockname, &statbuf) == 0 &&
+ fstat(mbx_lockfd, &ostatbuf) == 0 &&
+ statbuf.st_dev == ostatbuf.st_dev &&
+ statbuf.st_ino == ostatbuf.st_ino)
+ break;
+ DEBUG(D_transport) debug_printf("MBX lockfile %s changed "
+ "between creation and locking\n", mbx_lockname);
+ }
+
+ DEBUG(D_transport) debug_printf("failed to lock %s: %s\n", mbx_lockname,
+ strerror(errno));
+ (void)close(mbx_lockfd);
+ mbx_lockfd = -1;
+ }
+ else
+ {
+ DEBUG(D_transport) debug_printf("failed to fstat or get read lock on %s: %s\n",
+ filename, strerror(errno));
+ }
+ }
+ #endif /* SUPPORT_MBX */
+
+ else break; /* No on-file locking required; break the open/lock loop */
+
+ DEBUG(D_transport)
+ debug_printf("fcntl(), flock(), or MBX locking failed - retrying\n");
+
+ (void)close(fd);
+ fd = -1;
+ use_lstat = TRUE; /* Reset to use lstat first */
+
+
+ /* If a blocking call timed out, break the retry loop if the total time
+ so far is not less than than retries * interval. Use the larger of the
+ flock() and fcntl() timeouts. */
+
+ if (sigalrm_seen &&
+ (i+1) * ((ob->lock_fcntl_timeout > ob->lock_flock_timeout)?
+ ob->lock_fcntl_timeout : ob->lock_flock_timeout) >=
+ ob->lock_retries * ob->lock_interval)
+ i = ob->lock_retries;
+
+ /* Wait a bit before retrying, except when it was a blocked fcntl() or
+ flock() that caused the problem. */
+
+ if (i < ob->lock_retries && sleep_before_retry) sleep(ob->lock_interval);
+ }
+
+ /* Test for exceeding the maximum number of tries. Either the file remains
+ locked, or, if we haven't got it open, something is terribly wrong... */
+
+ if (i >= ob->lock_retries)
+ {
+ if (!file_opened)
+ {
+ addr->basic_errno = ERRNO_EXISTRACE;
+ addr->message = string_sprintf("mailbox %s: existence unclear", filename);
+ addr->special_action = SPECIAL_FREEZE;
+ }
+ else
+ {
+ addr->basic_errno = ERRNO_LOCKFAILED;
+ addr->message = string_sprintf("failed to lock mailbox %s (fcntl/flock)",
+ filename);
+ }
+ goto RETURN;
+ }
+
+ DEBUG(D_transport) debug_printf("mailbox %s is locked\n", filename);
+
+ /* Save access time (for subsequent restoration), modification time (for
+ restoration if updating fails), size of file (for comsat and for re-setting if
+ delivery fails in the middle - e.g. for quota exceeded). */
+
+ if (fstat(fd, &statbuf) < 0)
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while fstatting opened mailbox %s",
+ filename);
+ goto RETURN;
+ }
+
+ times.actime = statbuf.st_atime;
+ times.modtime = statbuf.st_mtime;
+ saved_size = statbuf.st_size;
+ if (mailbox_size < 0) mailbox_size = saved_size;
+ mailbox_filecount = 0; /* Not actually relevant for single-file mailbox */
+ }
+
+/* Prepare for writing to a new file (as opposed to appending to an old one).
+There are several different formats, but there is preliminary stuff concerned
+with quotas that applies to all of them. Finding the current size by directory
+scanning is expensive; for maildirs some fudges have been invented:
+
+ (1) A regex can be used to extract a file size from its name;
+ (2) If maildir_use_size is set, a maildirsize file is used to cache the
+ mailbox size.
+*/
+
+else
+ {
+ uschar *check_path; /* Default quota check path */
+ const pcre2_code * re = NULL; /* Regex for file size from file name */
+
+ if (!check_creation(string_sprintf("%s/any", path),
+ ob->create_file, deliver_dir))
+ {
+ addr->basic_errno = ERRNO_BADCREATE;
+ addr->message = string_sprintf("tried to create file in %s, but "
+ "file creation outside the home directory is not permitted", path);
+ goto RETURN;
+ }
+
+ /* If the create_file option is an absolute path and the file was within
+ it, de-taint. Otherwise check for taint. */
+
+ if (is_tainted(path))
+ if (ob->create_file == create_belowhome)
+ {
+ DEBUG(D_transport) debug_printf("de-tainting path '%s'\n", path);
+ path = string_copy_taint(path, GET_UNTAINTED);
+ }
+ else
+ goto tainted_ret_panic;
+
+ check_path = path;
+
+ #ifdef SUPPORT_MAILDIR
+ /* For a maildir delivery, ensure that all the relevant directories exist,
+ and a maildirfolder file if necessary. */
+
+ if (mbformat == mbf_maildir && !maildir_ensure_directories(path, addr,
+ ob->create_directory, ob->dirmode, ob->maildirfolder_create_regex))
+ return FALSE;
+ #endif /* SUPPORT_MAILDIR */
+
+ /* If we are going to do a quota check, of if maildir_use_size_file is set
+ for a maildir delivery, compile the regular expression if there is one. We
+ may also need to adjust the path that is used. We need to do this for
+ maildir_use_size_file even if the quota is unset, because we still want to
+ create the file. When maildir support is not compiled,
+ ob->maildir_use_size_file is always FALSE. */
+
+ if (ob->quota_value > 0 || THRESHOLD_CHECK || ob->maildir_use_size_file)
+ {
+ PCRE2_SIZE offset;
+ int err;
+
+ /* Compile the regex if there is one. */
+
+ if (ob->quota_size_regex)
+ {
+ if (!(re = pcre2_compile((PCRE2_SPTR)ob->quota_size_regex,
+ PCRE2_ZERO_TERMINATED, PCRE_COPT, &err, &offset, pcre_cmp_ctx)))
+ {
+ uschar errbuf[128];
+ pcre2_get_error_message(err, errbuf, sizeof(errbuf));
+ addr->message = string_sprintf("appendfile: regular expression "
+ "error: %s at offset %ld while compiling %s", errbuf, (long)offset,
+ ob->quota_size_regex);
+ return FALSE;
+ }
+ DEBUG(D_transport) debug_printf("using regex for file sizes: %s\n",
+ ob->quota_size_regex);
+ }
+
+ /* Use an explicitly configured directory if set */
+
+ if (ob->quota_directory)
+ {
+ if (!(check_path = expand_string(ob->quota_directory)))
+ {
+ addr->message = string_sprintf("Expansion of \"%s\" (quota_directory "
+ "name for %s transport) failed: %s", ob->quota_directory,
+ tblock->name, expand_string_message);
+ goto ret_panic;
+ }
+
+ if (check_path[0] != '/')
+ {
+ addr->message = string_sprintf("appendfile: quota_directory name "
+ "\"%s\" is not absolute", check_path);
+ addr->basic_errno = ERRNO_NOTABSOLUTE;
+ return FALSE;
+ }
+ }
+
+ #ifdef SUPPORT_MAILDIR
+ /* Otherwise, if we are handling a maildir delivery, and the directory
+ contains a file called maildirfolder, this is a maildir++ feature telling
+ us that this is a sub-directory of the real inbox. We should therefore do
+ the quota check on the parent directory. Beware of the special case when
+ the directory name itself ends in a slash. */
+
+ else if (mbformat == mbf_maildir)
+ {
+ struct stat statbuf;
+ if (Ustat(string_sprintf("%s/maildirfolder", path), &statbuf) >= 0)
+ {
+ uschar *new_check_path = string_copy(check_path);
+ uschar *slash = Ustrrchr(new_check_path, '/');
+ if (slash)
+ {
+ if (!slash[1])
+ {
+ *slash = 0;
+ slash = Ustrrchr(new_check_path, '/');
+ }
+ if (slash)
+ {
+ *slash = 0;
+ check_path = new_check_path;
+ DEBUG(D_transport) debug_printf("maildirfolder file exists: "
+ "quota check directory changed to %s\n", check_path);
+ }
+ }
+ }
+ }
+ #endif /* SUPPORT_MAILDIR */
+ }
+
+ /* If we are using maildirsize files, we need to ensure that such a file
+ exists and, if necessary, recalculate its contents. As a byproduct of this,
+ we obtain the current size of the maildir. If no quota is to be enforced
+ (ob->quota_value == 0), we still need the size if a threshold check will
+ happen later.
+
+ Another regular expression is used to determine which directories inside the
+ maildir are going to be counted. */
+
+ #ifdef SUPPORT_MAILDIR
+ if (ob->maildir_use_size_file)
+ {
+ const pcre2_code * dir_regex = NULL;
+ PCRE2_SIZE offset;
+ int err;
+
+ if (ob->maildir_dir_regex)
+ {
+ int check_path_len = Ustrlen(check_path);
+
+ if (!(dir_regex = pcre2_compile((PCRE2_SPTR)ob->maildir_dir_regex,
+ PCRE2_ZERO_TERMINATED, PCRE_COPT, &err, &offset, pcre_cmp_ctx)))
+ {
+ uschar errbuf[128];
+ pcre2_get_error_message(err, errbuf, sizeof(errbuf));
+ addr->message = string_sprintf("appendfile: regular expression "
+ "error: %s at offset %ld while compiling %s", errbuf, (long)offset,
+ ob->maildir_dir_regex);
+ return FALSE;
+ }
+
+ DEBUG(D_transport)
+ debug_printf("using regex for maildir directory selection: %s\n",
+ ob->maildir_dir_regex);
+
+ /* Check to see if we are delivering into an ignored directory, that is,
+ if the delivery path starts with the quota check path, and the rest
+ of the deliver path matches the regex; if so, set a flag to disable quota
+ checking and maildirsize updating. */
+
+ if (Ustrncmp(path, check_path, check_path_len) == 0)
+ {
+ uschar *s = path + check_path_len;
+ while (*s == '/') s++;
+ s = *s ? string_sprintf("%s/new", s) : US"new";
+ if (!regex_match(dir_regex, s, -1, NULL))
+ {
+ disable_quota = TRUE;
+ DEBUG(D_transport) debug_printf("delivery directory does not match "
+ "maildir_quota_directory_regex: disabling quota\n");
+ }
+ }
+ }
+
+ /* Quota enforcement; create and check the file. There is some discussion
+ about whether this should happen if the quota is unset. At present, Exim
+ always creates the file. If we ever want to change this, uncomment
+ appropriate lines below, possibly doing a check on some option. */
+
+/* if (???? || ob->quota_value > 0) */
+
+ if (!disable_quota)
+ {
+ off_t size;
+ int filecount;
+
+ if ((maildirsize_fd = maildir_ensure_sizefile(check_path, ob, re, dir_regex,
+ &size, &filecount)) == -1)
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while opening or reading "
+ "%s/maildirsize", check_path);
+ return FALSE;
+ }
+ /* can also return -2, which means that the file was removed because of
+ raciness; but in this case, the size & filecount will still have been
+ updated. */
+
+ if (mailbox_size < 0) mailbox_size = size;
+ if (mailbox_filecount < 0) mailbox_filecount = filecount;
+ }
+
+ /* No quota enforcement; ensure file does *not* exist; calculate size if
+ needed. */
+
+/* else
+ * {
+ * time_t old_latest;
+ * (void)unlink(CS string_sprintf("%s/maildirsize", check_path));
+ * if (THRESHOLD_CHECK)
+ * mailbox_size = maildir_compute_size(check_path, &mailbox_filecount, &old_latest,
+ * re, dir_regex, FALSE);
+ * }
+*/
+
+ }
+ #endif /* SUPPORT_MAILDIR */
+
+ /* Otherwise if we are going to do a quota check later on, and the mailbox
+ size is not set, find the current size of the mailbox. Ditto for the file
+ count. Note that ob->quota_filecount_value cannot be set without
+ ob->quota_value being set. */
+
+ if ( !disable_quota
+ && (ob->quota_value > 0 || THRESHOLD_CHECK)
+ && ( mailbox_size < 0
+ || mailbox_filecount < 0 && ob->quota_filecount_value > 0
+ ) )
+ {
+ off_t size;
+ int filecount = 0;
+ DEBUG(D_transport)
+ debug_printf("quota checks on directory %s\n", check_path);
+ size = check_dir_size(check_path, &filecount, re);
+ if (mailbox_size < 0) mailbox_size = size;
+ if (mailbox_filecount < 0) mailbox_filecount = filecount;
+ }
+
+ /* Handle the case of creating a unique file in a given directory (not in
+ maildir or mailstore format - this is how smail did it). A temporary name is
+ used to create the file. Later, when it is written, the name is changed to a
+ unique one. There is no need to lock the file. An attempt is made to create
+ the directory if it does not exist. */
+
+ if (mbformat == mbf_smail)
+ {
+ DEBUG(D_transport)
+ debug_printf("delivering to new file in %s\n", path);
+ filename = dataname =
+ string_sprintf("%s/temp.%d.%s", path, (int)getpid(), primary_hostname);
+ fd = Uopen(filename, O_WRONLY|O_CREAT, mode);
+ if (fd < 0 && /* failed to open, and */
+ (errno != ENOENT || /* either not non-exist */
+ !ob->create_directory || /* or not allowed to make */
+ !directory_make(NULL, path, ob->dirmode, FALSE) || /* or failed to create dir */
+ (fd = Uopen(filename, O_WRONLY|O_CREAT|O_EXCL, mode)) < 0)) /* or then failed to open */
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while creating file %s", filename);
+ return FALSE;
+ }
+ }
+
+ #ifdef SUPPORT_MAILDIR
+
+ /* Handle the case of a unique file in maildir format. The file is written to
+ the tmp subdirectory, with a prescribed form of name. */
+
+ else if (mbformat == mbf_maildir)
+ {
+ DEBUG(D_transport)
+ debug_printf("delivering in maildir format in %s\n", path);
+
+ nametag = ob->maildir_tag;
+
+ /* Check that nametag expands successfully; a hard failure causes a panic
+ return. The actual expansion for use happens again later, when
+ $message_size is accurately known. */
+
+ if (nametag && !expand_string(nametag) && !f.expand_string_forcedfail)
+ {
+ addr->message = string_sprintf("Expansion of \"%s\" (maildir_tag "
+ "for %s transport) failed: %s", nametag, tblock->name,
+ expand_string_message);
+ goto ret_panic;
+ }
+
+ /* We ensured the existence of all the relevant directories above. Attempt
+ to open the temporary file a limited number of times. I think this rather
+ scary-looking for statement is actually OK. If open succeeds, the loop is
+ broken; if not, there is a test on the value of i. Get the time again
+ afresh each time round the loop. Its value goes into a variable that is
+ checked at the end, to make sure we don't release this process until the
+ clock has ticked. */
+
+ for (int i = 1;; i++)
+ {
+ uschar *basename;
+
+ (void)gettimeofday(&msg_tv, NULL);
+ basename = string_sprintf(TIME_T_FMT ".M%luP" PID_T_FMT ".%s",
+ msg_tv.tv_sec, msg_tv.tv_usec, getpid(), primary_hostname);
+
+ filename = dataname = string_sprintf("tmp/%s", basename);
+ newname = string_sprintf("new/%s", basename);
+
+ if (Ustat(filename, &statbuf) == 0)
+ errno = EEXIST;
+ else if (errno == ENOENT)
+ {
+ if ((fd = Uopen(filename, O_WRONLY | O_CREAT | O_EXCL, mode)) >= 0)
+ break;
+ DEBUG (D_transport) debug_printf ("open failed for %s: %s\n",
+ filename, strerror(errno));
+ }
+
+ /* Too many retries - give up */
+
+ if (i >= ob->maildir_retries)
+ {
+ addr->message = string_sprintf ("failed to open %s (%d tr%s)",
+ filename, i, (i == 1) ? "y" : "ies");
+ addr->basic_errno = errno;
+ if (errno == errno_quota || errno == ENOSPC)
+ addr->user_message = US"mailbox is full";
+ return FALSE;
+ }
+
+ /* Open or stat failed but we haven't tried too many times yet. */
+
+ sleep(2);
+ }
+
+ /* Note that we have to ensure the clock has ticked before leaving */
+
+ wait_for_tick = TRUE;
+
+ /* Why are these here? Put in because they are present in the non-maildir
+ directory case above. */
+
+ if (exim_chown(filename, uid, gid) || Uchmod(filename, mode))
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while setting perms on maildir %s",
+ filename);
+ return FALSE;
+ }
+ }
+
+ #endif /* SUPPORT_MAILDIR */
+
+ #ifdef SUPPORT_MAILSTORE
+
+ /* Handle the case of a unique file in mailstore format. First write the
+ envelope to a temporary file, then open the main file. The unique base name
+ for the files consists of the message id plus the pid of this delivery
+ process. */
+
+ else
+ {
+ FILE *env_file;
+ mailstore_basename = string_sprintf("%s/%s-%s", path, message_id,
+ string_base62((long int)getpid()));
+
+ DEBUG(D_transport)
+ debug_printf("delivering in mailstore format in %s\n", path);
+
+ filename = string_sprintf("%s.tmp", mailstore_basename);
+ newname = string_sprintf("%s.env", mailstore_basename);
+ dataname = string_sprintf("%s.msg", mailstore_basename);
+
+ fd = Uopen(filename, O_WRONLY|O_CREAT|O_EXCL, mode);
+ if ( fd < 0 /* failed to open, and */
+ && ( errno != ENOENT /* either not non-exist */
+ || !ob->create_directory /* or not allowed to make */
+ || !directory_make(NULL, path, ob->dirmode, FALSE) /* or failed to create dir */
+ || (fd = Uopen(filename, O_WRONLY|O_CREAT|O_EXCL, mode)) < 0 /* or then failed to open */
+ ) )
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while creating file %s", filename);
+ return FALSE;
+ }
+
+ /* Why are these here? Put in because they are present in the non-maildir
+ directory case above. */
+
+ if (exim_chown(filename, uid, gid) || Uchmod(filename, mode))
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while setting perms on file %s",
+ filename);
+ return FALSE;
+ }
+
+ /* Built a C stream from the open file descriptor. */
+
+ if (!(env_file = fdopen(fd, "wb")))
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("fdopen of %s ("
+ "for %s transport) failed", filename, tblock->name);
+ (void)close(fd);
+ Uunlink(filename);
+ goto ret_panic;
+ }
+
+ /* Write the envelope file, then close it. */
+
+ if (ob->mailstore_prefix)
+ {
+ uschar *s = expand_string(ob->mailstore_prefix);
+ if (!s)
+ {
+ if (!f.expand_string_forcedfail)
+ {
+ addr->message = string_sprintf("Expansion of \"%s\" (mailstore "
+ "prefix for %s transport) failed: %s", ob->mailstore_prefix,
+ tblock->name, expand_string_message);
+ (void)fclose(env_file);
+ Uunlink(filename);
+ goto ret_panic;
+ }
+ }
+ else
+ {
+ int n = Ustrlen(s);
+ fprintf(env_file, "%s", CS s);
+ if (n == 0 || s[n-1] != '\n') fprintf(env_file, "\n");
+ }
+ }
+
+ fprintf(env_file, "%s\n", sender_address);
+
+ for (address_item * taddr = addr; taddr; taddr = taddr->next)
+ fprintf(env_file, "%s@%s\n", taddr->local_part, taddr->domain);
+
+ if (ob->mailstore_suffix)
+ {
+ uschar *s = expand_string(ob->mailstore_suffix);
+ if (!s)
+ {
+ if (!f.expand_string_forcedfail)
+ {
+ addr->message = string_sprintf("Expansion of \"%s\" (mailstore "
+ "suffix for %s transport) failed: %s", ob->mailstore_suffix,
+ tblock->name, expand_string_message);
+ (void)fclose(env_file);
+ Uunlink(filename);
+ goto ret_panic;
+ }
+ }
+ else
+ {
+ int n = Ustrlen(s);
+ fprintf(env_file, "%s", CS s);
+ if (n == 0 || s[n-1] != '\n') fprintf(env_file, "\n");
+ }
+ }
+
+ if (fclose(env_file) != 0)
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while closing %s", filename);
+ Uunlink(filename);
+ return FALSE;
+ }
+
+ DEBUG(D_transport) debug_printf("Envelope file %s written\n", filename);
+
+ /* Now open the data file, and ensure that it has the correct ownership and
+ mode. */
+
+ if ((fd = Uopen(dataname, O_WRONLY|O_CREAT|O_EXCL, mode)) < 0)
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while creating file %s", dataname);
+ Uunlink(filename);
+ return FALSE;
+ }
+ if (exim_chown(dataname, uid, gid) || Uchmod(dataname, mode))
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while setting perms on file %s",
+ dataname);
+ return FALSE;
+ }
+ }
+
+ #endif /* SUPPORT_MAILSTORE */
+
+
+ /* In all cases of writing to a new file, ensure that the file which is
+ going to be renamed has the correct ownership and mode. */
+
+ if (exim_chown(filename, uid, gid) || Uchmod(filename, mode))
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while setting perms on file %s",
+ filename);
+ return FALSE;
+ }
+ }
+
+
+/* At last we can write the message to the file, preceded by any configured
+prefix line, and followed by any configured suffix line. If there are any
+writing errors, we must defer. */
+
+DEBUG(D_transport) debug_printf("writing to file %s\n", dataname);
+
+yield = OK;
+errno = 0;
+
+/* If there is a local quota setting, check that we are not going to exceed it
+with this message if quota_is_inclusive is set; if it is not set, the check
+is for the mailbox already being over quota (i.e. the current message is not
+included in the check). */
+
+if (!disable_quota && ob->quota_value > 0)
+ {
+ DEBUG(D_transport)
+ {
+ debug_printf("Exim quota = " OFF_T_FMT " old size = " OFF_T_FMT
+ " this message = %d (%sincluded)\n",
+ ob->quota_value, mailbox_size, message_size,
+ ob->quota_is_inclusive ? "" : "not ");
+ debug_printf(" file count quota = %d count = %d\n",
+ ob->quota_filecount_value, mailbox_filecount);
+ }
+
+ if (mailbox_size + (ob->quota_is_inclusive ? message_size:0) > ob->quota_value)
+ if (!ob->quota_no_check)
+ {
+ DEBUG(D_transport) debug_printf("mailbox quota exceeded\n");
+ yield = DEFER;
+ errno = ERRNO_EXIMQUOTA;
+ }
+ else
+ DEBUG(D_transport) debug_printf("mailbox quota exceeded but ignored\n");
+
+ if (ob->quota_filecount_value > 0
+ && mailbox_filecount + (ob->quota_is_inclusive ? 1:0) >
+ ob->quota_filecount_value)
+ if (!ob->quota_filecount_no_check)
+ {
+ DEBUG(D_transport) debug_printf("mailbox file count quota exceeded\n");
+ yield = DEFER;
+ errno = ERRNO_EXIMQUOTA;
+ filecount_msg = US" filecount";
+ }
+ else DEBUG(D_transport) if (ob->quota_filecount_no_check)
+ debug_printf("mailbox file count quota exceeded but ignored\n");
+
+ }
+
+if (verify_mode)
+ {
+ addr->basic_errno = errno;
+ addr->message = US"Over quota";
+ addr->transport_return = yield;
+ DEBUG(D_transport)
+ debug_printf("appendfile (verify) yields %d with errno=%d more_errno=%d\n",
+ yield, addr->basic_errno, addr->more_errno);
+
+ goto RETURN;
+ }
+
+/* If we are writing in MBX format, what we actually do is to write the message
+to a temporary file, and then copy it to the real file once we know its size.
+This is the most straightforward way of getting the correct length in the
+separator line. So, what we do here is to save the real file descriptor, and
+replace it with one for a temporary file. The temporary file gets unlinked once
+opened, so that it goes away on closure. */
+
+#ifdef SUPPORT_MBX
+if (yield == OK && ob->mbx_format)
+ {
+ if (!(temp_file = tmpfile()))
+ {
+ addr->basic_errno = errno;
+ addr->message = US"while setting up temporary file";
+ yield = DEFER;
+ goto RETURN;
+ }
+ save_fd = fd;
+ fd = fileno(temp_file);
+ DEBUG(D_transport) debug_printf("writing to temporary file\n");
+ }
+#endif /* SUPPORT_MBX */
+
+/* Zero the count of bytes written. It is incremented by the transport_xxx()
+functions. */
+
+transport_count = 0;
+transport_newlines = 0;
+
+/* Write any configured prefix text first */
+
+if (yield == OK && ob->message_prefix && *ob->message_prefix)
+ {
+ uschar *prefix = expand_string(ob->message_prefix);
+ if (!prefix)
+ {
+ errno = ERRNO_EXPANDFAIL;
+ addr->transport_return = PANIC;
+ addr->message = string_sprintf("Expansion of \"%s\" (prefix for %s "
+ "transport) failed", ob->message_prefix, tblock->name);
+ yield = DEFER;
+ }
+ else if (!transport_write_string(fd, "%s", prefix)) yield = DEFER;
+ }
+
+/* If the use_bsmtp option is on, we need to write SMTP prefix information. The
+various different values for batching are handled outside; if there is more
+than one address available here, all must be included. If any address is a
+file, use its parent in the RCPT TO. */
+
+if (yield == OK && ob->use_bsmtp)
+ {
+ transport_count = 0;
+ transport_newlines = 0;
+ if (ob->use_crlf) cr = US"\r";
+ if (!transport_write_string(fd, "MAIL FROM:<%s>%s\n", return_path, cr))
+ yield = DEFER;
+ else
+ {
+ transport_newlines++;
+ for (address_item * a = addr; a; a = a->next)
+ {
+ address_item * b = testflag(a, af_pfr) ? a->parent : a;
+ if (!transport_write_string(fd, "RCPT TO:<%s>%s\n",
+ transport_rcpt_address(b, tblock->rcpt_include_affixes), cr))
+ { yield = DEFER; break; }
+ transport_newlines++;
+ }
+ if (yield == OK && !transport_write_string(fd, "DATA%s\n", cr))
+ yield = DEFER;
+ else
+ transport_newlines++;
+ }
+ }
+
+/* Now the message itself. The options for transport_write_message were set up
+at initialization time. */
+
+if (yield == OK)
+ {
+ transport_ctx tctx = {
+ .u = {.fd=fd},
+ .tblock = tblock,
+ .addr = addr,
+ .check_string = ob->check_string,
+ .escape_string = ob->escape_string,
+ .options = ob->options | topt_not_socket
+ };
+ if (!transport_write_message(&tctx, 0))
+ yield = DEFER;
+ }
+
+/* Now a configured suffix. */
+
+if (yield == OK && ob->message_suffix && *ob->message_suffix)
+ {
+ uschar *suffix = expand_string(ob->message_suffix);
+ if (!suffix)
+ {
+ errno = ERRNO_EXPANDFAIL;
+ addr->transport_return = PANIC;
+ addr->message = string_sprintf("Expansion of \"%s\" (suffix for %s "
+ "transport) failed", ob->message_suffix, tblock->name);
+ yield = DEFER;
+ }
+ else if (!transport_write_string(fd, "%s", suffix)) yield = DEFER;
+ }
+
+/* If batch smtp, write the terminating dot. */
+
+if (yield == OK && ob->use_bsmtp)
+ if (!transport_write_string(fd, ".%s\n", cr)) yield = DEFER;
+ else transport_newlines++;
+
+/* If MBX format is being used, all that writing was to the temporary file.
+However, if there was an earlier failure (Exim quota exceeded, for example),
+the temporary file won't have got opened - and no writing will have been done.
+If writing was OK, we restore the fd, and call a function that copies the
+message in MBX format into the real file. Otherwise use the temporary name in
+any messages. */
+
+#ifdef SUPPORT_MBX
+if (temp_file && ob->mbx_format)
+ {
+ int mbx_save_errno;
+ fd = save_fd;
+
+ if (yield == OK)
+ {
+ transport_count = 0; /* Reset transport count for actual write */
+ /* No need to reset transport_newlines as we're just using a block copy
+ * routine so the number won't be affected */
+ yield = copy_mbx_message(fd, fileno(temp_file), saved_size);
+ }
+ else if (errno >= 0) dataname = US"temporary file";
+
+ /* Preserve errno while closing the temporary file. */
+
+ mbx_save_errno = errno;
+ (void)fclose(temp_file);
+ errno = mbx_save_errno;
+ }
+#endif /* SUPPORT_MBX */
+
+/* Force out the remaining data to check for any errors; some OS don't allow
+fsync() to be called for a FIFO. */
+
+if (yield == OK && !isfifo && EXIMfsync(fd) < 0) yield = DEFER;
+
+/* Update message_size and message_linecount to the accurate count of bytes
+written, including added headers. Note; we subtract 1 from message_linecount as
+this variable doesn't count the new line between the header and the body of the
+message. */
+
+message_size = transport_count;
+message_linecount = transport_newlines - 1;
+
+/* If using a maildir++ quota file, add this message's size to it, and
+close the file descriptor, except when the quota has been disabled because we
+are delivering into an uncounted folder. */
+
+#ifdef SUPPORT_MAILDIR
+if (!disable_quota)
+ {
+ if (yield == OK && maildirsize_fd >= 0)
+ maildir_record_length(maildirsize_fd, message_size);
+ maildir_save_errno = errno; /* Preserve errno while closing the file */
+ if (maildirsize_fd >= 0)
+ (void)close(maildirsize_fd);
+ errno = maildir_save_errno;
+ }
+#endif /* SUPPORT_MAILDIR */
+
+/* If there is a quota warning threshold and we are have crossed it with this
+message, set the SPECIAL_WARN flag in the address, to cause a warning message
+to be sent. */
+
+if (!disable_quota && THRESHOLD_CHECK)
+ {
+ off_t threshold = ob->quota_warn_threshold_value;
+ if (ob->quota_warn_threshold_is_percent)
+ threshold = (off_t)(((double)ob->quota_value * threshold) / 100);
+ DEBUG(D_transport)
+ debug_printf("quota = " OFF_T_FMT
+ " threshold = " OFF_T_FMT
+ " old size = " OFF_T_FMT
+ " message size = %d\n",
+ ob->quota_value, threshold, mailbox_size,
+ message_size);
+ if (mailbox_size <= threshold && mailbox_size + message_size > threshold)
+ addr->special_action = SPECIAL_WARN;
+
+ /******* You might think that the test ought to be this:
+ *
+ * if (ob->quota_value > 0 && threshold > 0 && mailbox_size > 0 &&
+ * mailbox_size <= threshold && mailbox_size + message_size > threshold)
+ *
+ * (indeed, I was sent a patch with that in). However, it is possible to
+ * have a warning threshold without actually imposing a quota, and I have
+ * therefore kept Exim backwards compatible.
+ ********/
+
+ }
+
+/* Handle error while writing the file. Control should come here directly after
+the error, with the reason in errno. In the case of expansion failure in prefix
+or suffix, it will be ERRNO_EXPANDFAIL. */
+
+if (yield != OK)
+ {
+ addr->special_action = SPECIAL_NONE; /* Cancel any quota warning */
+
+ /* Save the error number. If positive, it will ultimately cause a strerror()
+ call to generate some text. */
+
+ addr->basic_errno = errno;
+
+ /* For system or Exim quota excession, or disk full, set more_errno to the
+ time since the file was last read. If delivery was into a directory, the
+ time since last read logic is not relevant, in general. However, for maildir
+ deliveries we can approximate it by looking at the last modified time of the
+ "new" subdirectory. Since Exim won't be adding new messages, a change to the
+ "new" subdirectory implies that an MUA has moved a message from there to the
+ "cur" directory. */
+
+ if (errno == errno_quota || errno == ERRNO_EXIMQUOTA || errno == ENOSPC)
+ {
+ addr->more_errno = 0;
+ if (!isdirectory) addr->more_errno = (int)(time(NULL) - times.actime);
+
+ #ifdef SUPPORT_MAILDIR
+ else if (mbformat == mbf_maildir)
+ {
+ struct stat statbuf;
+ if (Ustat("new", &statbuf) < 0)
+ {
+ DEBUG(D_transport) debug_printf("maildir quota exceeded: "
+ "stat error %d for \"new\": %s\n", errno, strerror(errno));
+ }
+ else /* Want a repeatable time when in test harness */
+ addr->more_errno = f.running_in_test_harness ? 10 :
+ (int)time(NULL) - statbuf.st_mtime;
+
+ DEBUG(D_transport)
+ debug_printf("maildir: time since \"new\" directory modified = %s\n",
+ readconf_printtime(addr->more_errno));
+ }
+ #endif /* SUPPORT_MAILDIR */
+ }
+
+ /* Handle system quota excession. Add an explanatory phrase for the error
+ message, since some systems don't have special quota-excession errors,
+ and on those that do, "quota" doesn't always mean anything to the user. */
+
+ if (errno == errno_quota)
+ {
+ #ifndef EDQUOT
+ addr->message = string_sprintf("mailbox is full "
+ "(quota exceeded while writing to file %s)", filename);
+ #else
+ addr->message = US"mailbox is full";
+ #endif /* EDQUOT */
+ addr->user_message = US"mailbox is full";
+ DEBUG(D_transport) debug_printf("System quota exceeded for %s%s%s\n",
+ dataname,
+ isdirectory ? US"" : US": time since file read = ",
+ isdirectory ? US"" : readconf_printtime(addr->more_errno));
+ }
+
+ /* Handle Exim's own quota-imposition */
+
+ else if (errno == ERRNO_EXIMQUOTA)
+ {
+ addr->message = string_sprintf("mailbox is full "
+ "(MTA-imposed%s quota exceeded while writing to %s)", filecount_msg,
+ dataname);
+ addr->user_message = US"mailbox is full";
+ DEBUG(D_transport) debug_printf("Exim%s quota exceeded for %s%s%s\n",
+ filecount_msg, dataname,
+ isdirectory ? US"" : US": time since file read = ",
+ isdirectory ? US"" : readconf_printtime(addr->more_errno));
+ }
+
+ /* Handle a process failure while writing via a filter; the return
+ from child_close() is in more_errno. */
+
+ else if (errno == ERRNO_FILTER_FAIL)
+ {
+ yield = PANIC;
+ addr->message = string_sprintf("transport filter process failed (%d) "
+ "while writing to %s%s", addr->more_errno, dataname,
+ (addr->more_errno == EX_EXECFAILED) ? ": unable to execute command" : "");
+ }
+
+ /* Handle failure to expand header changes */
+
+ else if (errno == ERRNO_CHHEADER_FAIL)
+ {
+ yield = PANIC;
+ addr->message =
+ string_sprintf("failed to expand headers_add or headers_remove while "
+ "writing to %s: %s", dataname, expand_string_message);
+ }
+
+ /* Handle failure to complete writing of a data block */
+
+ else if (errno == ERRNO_WRITEINCOMPLETE)
+ addr->message = string_sprintf("failed to write data block while "
+ "writing to %s", dataname);
+
+ /* Handle length mismatch on MBX copying */
+
+ #ifdef SUPPORT_MBX
+ else if (errno == ERRNO_MBXLENGTH)
+ addr->message = string_sprintf("length mismatch while copying MBX "
+ "temporary file to %s", dataname);
+ #endif /* SUPPORT_MBX */
+
+ /* For other errors, a general-purpose explanation, if the message is
+ not already set. */
+
+ else if (addr->message == NULL)
+ addr->message = string_sprintf("error while writing to %s", dataname);
+
+ /* For a file, reset the file size to what it was before we started, leaving
+ the last modification time unchanged, so it will get reset also. All systems
+ investigated so far have ftruncate(), whereas not all have the F_FREESP
+ fcntl() call (BSDI & FreeBSD do not). */
+
+ if (!isdirectory && ftruncate(fd, saved_size))
+ DEBUG(D_transport) debug_printf("Error resetting file size\n");
+ }
+
+/* Handle successful writing - we want the modification time to be now for
+appended files. Remove the default backstop error number. For a directory, now
+is the time to rename the file with a unique name. As soon as such a name
+appears it may get used by another process, so we close the file first and
+check that all is well. */
+
+else
+ {
+ times.modtime = time(NULL);
+ addr->basic_errno = 0;
+
+ /* Handle the case of writing to a new file in a directory. This applies
+ to all single-file formats - maildir, mailstore, and "smail format". */
+
+ if (isdirectory)
+ {
+ if (fstat(fd, &statbuf) < 0)
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while fstatting opened message file %s",
+ filename);
+ yield = DEFER;
+ }
+
+ else if (close(fd) < 0)
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("close() error for %s",
+ (ob->mailstore_format) ? dataname : filename);
+ yield = DEFER;
+ }
+
+ /* File is successfully written and closed. Arrange to rename it. For the
+ different kinds of single-file delivery, some games can be played with the
+ name. The message size is by this time set to the accurate value so that
+ its value can be used in expansions. */
+
+ else
+ {
+ uschar *renamename = newname;
+ fd = -1;
+
+ DEBUG(D_transport) debug_printf("renaming temporary file\n");
+
+ /* If there is no rename name set, we are in a non-maildir, non-mailstore
+ situation. The name is built by expanding the directory_file option, and
+ we make the inode number available for use in this. The expansion was
+ checked for syntactic validity above, before we wrote the file.
+
+ We have to be careful here, in case the file name exists. (In the other
+ cases, the names used are constructed to be unique.) The rename()
+ function just replaces an existing file - we don't want that! So instead
+ of calling rename(), we must use link() and unlink().
+
+ In this case, if the link fails because of an existing file, we wait
+ for one second and try the expansion again, to see if it produces a
+ different value. Do this up to 5 times unless the name stops changing.
+ This makes it possible to build values that are based on the time, and
+ still cope with races from multiple simultaneous deliveries. */
+
+ if (!newname)
+ {
+ uschar *renameleaf;
+ uschar *old_renameleaf = US"";
+
+ for (int i = 0; ; sleep(1), i++)
+ {
+ deliver_inode = statbuf.st_ino;
+ renameleaf = expand_string(ob->dirfilename);
+ deliver_inode = 0;
+
+ if (!renameleaf)
+ {
+ addr->transport_return = PANIC;
+ addr->message = string_sprintf("Expansion of \"%s\" "
+ "(directory_file for %s transport) failed: %s",
+ ob->dirfilename, tblock->name, expand_string_message);
+ goto RETURN;
+ }
+
+ renamename = string_sprintf("%s/%s", path, renameleaf);
+ if (Ulink(filename, renamename) < 0)
+ {
+ DEBUG(D_transport) debug_printf("link failed: %s\n",
+ strerror(errno));
+ if (errno != EEXIST || i >= 4 ||
+ Ustrcmp(renameleaf, old_renameleaf) == 0)
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while renaming %s as %s",
+ filename, renamename);
+ yield = DEFER;
+ break;
+ }
+ old_renameleaf = renameleaf;
+ DEBUG(D_transport) debug_printf("%s exists - trying again\n",
+ renamename);
+ }
+ else
+ {
+ Uunlink(filename);
+ filename = NULL;
+ break;
+ }
+ } /* re-expand loop */
+ } /* not mailstore or maildir */
+
+ /* For maildir and mailstore formats, the new name was created earlier,
+ except that for maildir, there is the possibility of adding a "tag" on
+ the end of the name by expanding the value of nametag. This usually
+ includes a reference to the message size. The expansion of nametag was
+ checked above, before the file was opened. It either succeeded, or
+ provoked a soft failure. So any failure here can be treated as soft.
+ Ignore non-printing characters and / and put a colon at the start if the
+ first character is alphanumeric. */
+
+ else
+ {
+ if (nametag)
+ {
+ uschar *iptr = expand_string(nametag);
+ if (iptr)
+ {
+ uschar *etag = store_get(Ustrlen(iptr) + 2, iptr);
+ uschar *optr = etag;
+ for ( ; *iptr; iptr++)
+ if (mac_isgraph(*iptr) && *iptr != '/')
+ {
+ if (optr == etag && isalnum(*iptr)) *optr++ = ':';
+ *optr++ = *iptr;
+ }
+ *optr = 0;
+ renamename = string_sprintf("%s%s", newname, etag);
+ }
+ }
+
+ /* Do the rename. If the name is too long and a tag exists, try again
+ without the tag. */
+
+ if (Urename(filename, renamename) < 0 &&
+ (nametag == NULL || errno != ENAMETOOLONG ||
+ (renamename = newname, Urename(filename, renamename) < 0)))
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while renaming %s as %s",
+ filename, renamename);
+ yield = DEFER;
+ }
+
+ /* Rename succeeded */
+
+ else
+ {
+ DEBUG(D_transport) debug_printf("renamed %s as %s\n", filename,
+ renamename);
+ filename = dataname = NULL; /* Prevents attempt to unlink at end */
+ }
+ } /* maildir or mailstore */
+ } /* successful write + close */
+ } /* isdirectory */
+ } /* write success */
+
+
+/* For a file, restore the last access time (atime), and set the modification
+time as required - changed if write succeeded, unchanged if not. */
+
+if (!isdirectory) utime(CS filename, &times);
+
+/* Notify comsat if configured to do so. It only makes sense if the configured
+file is the one that the comsat daemon knows about. */
+
+if (ob->notify_comsat && yield == OK && deliver_localpart)
+ notify_comsat(deliver_localpart, saved_size);
+
+/* Pass back the final return code in the address structure */
+
+DEBUG(D_transport)
+ debug_printf("appendfile yields %d with errno=%d more_errno=%d\n",
+ yield, addr->basic_errno, addr->more_errno);
+
+addr->transport_return = yield;
+
+/* Close the file, which will release the fcntl lock. For a directory write it
+is closed above, except in cases of error which goto RETURN, when we also need
+to remove the original file(s). For MBX locking, if all has gone well, before
+closing the file, see if we can get an exclusive lock on it, in which case we
+can unlink the /tmp lock file before closing it. This is always a non-blocking
+lock; there's no need to wait if we can't get it. If everything has gone right
+but close fails, defer the message. Then unlink the lock file, if present. This
+point in the code is jumped to from a number of places when errors are
+detected, in order to get the file closed and the lock file tidied away. */
+
+RETURN:
+
+#ifdef SUPPORT_MBX
+if (mbx_lockfd >= 0)
+ {
+ if (yield == OK && apply_lock(fd, F_WRLCK, ob->use_fcntl, 0,
+ ob->use_flock, 0) >= 0)
+ {
+ DEBUG(D_transport)
+ debug_printf("unlinking MBX lock file %s\n", mbx_lockname);
+ Uunlink(mbx_lockname);
+ }
+ (void)close(mbx_lockfd);
+ }
+#endif /* SUPPORT_MBX */
+
+if (fd >= 0 && close(fd) < 0 && yield == OK)
+ {
+ addr->basic_errno = errno;
+ addr->message = string_sprintf("while closing %s", filename);
+ addr->transport_return = DEFER;
+ }
+
+if (hd >= 0) Uunlink(lockname);
+
+/* We get here with isdirectory and filename set only in error situations. */
+
+if (isdirectory && filename)
+ {
+ Uunlink(filename);
+ if (dataname != filename) Uunlink(dataname);
+ }
+
+/* If wait_for_tick is TRUE, we have done a delivery where the uniqueness of a
+file name relies on time + pid. We must not allow the process to finish until
+the clock has move on by at least one microsecond. Usually we expect this
+already to be the case, but machines keep getting faster... */
+
+if (wait_for_tick) exim_wait_tick(&msg_tv, 1);
+
+/* A return of FALSE means that if there was an error, a common error was
+put in the first address of a batch. */
+
+return FALSE;
+
+tainted_ret_panic:
+ addr->message = string_sprintf("Tainted '%s' (file or directory "
+ "name for %s transport) not permitted", path, tblock->name);
+ret_panic:
+ addr->transport_return = PANIC;
+ return FALSE;
+}
+
+#endif /*!MACRO_PREDEF*/
+/* End of transport/appendfile.c */