+Returns: When the global variable mua_wrapper is FALSE:
+ DELIVER_ATTEMPTED_NORMAL if a delivery attempt was made
+ DELIVER_NOT_ATTEMPTED otherwise (see comment above)
+ When the global variable mua_wrapper is TRUE:
+ DELIVER_MUA_SUCCEEDED if delivery succeeded
+ DELIVER_MUA_FAILED if delivery failed
+ DELIVER_NOT_ATTEMPTED if not attempted (should not occur)
+*/
+
+int
+deliver_message(const uschar * id, BOOL forced, BOOL give_up)
+{
+int i, rc;
+int final_yield = DELIVER_ATTEMPTED_NORMAL;
+time_t now = time(NULL);
+address_item *addr_last = NULL;
+uschar *filter_message = NULL;
+int process_recipients = RECIP_ACCEPT;
+open_db dbblock;
+open_db *dbm_file;
+extern int acl_where;
+uschar *info;
+
+#ifdef MEASURE_TIMING
+report_time_since(×tamp_startup, US"delivery start"); /* testcase 0022, 2100 */
+#endif
+
+info = queue_run_pid == (pid_t)0
+ ? string_sprintf("delivering %s", id)
+ : string_sprintf("delivering %s (queue run pid %d)", id, queue_run_pid);
+
+/* If the D_process_info bit is on, set_process_info() will output debugging
+information. If not, we want to show this initial information if D_deliver or
+D_queue_run is set or in verbose mode. */
+
+set_process_info("%s", info);
+
+if ( !(debug_selector & D_process_info)
+ && (debug_selector & (D_deliver|D_queue_run|D_v))
+ )
+ debug_printf("%s\n", info);
+
+/* Ensure that we catch any subprocesses that are created. Although Exim
+sets SIG_DFL as its initial default, some routes through the code end up
+here with it set to SIG_IGN - cases where a non-synchronous delivery process
+has been forked, but no re-exec has been done. We use sigaction rather than
+plain signal() on those OS where SA_NOCLDWAIT exists, because we want to be
+sure it is turned off. (There was a problem on AIX with this.) */
+
+#ifdef SA_NOCLDWAIT
+ {
+ struct sigaction act;
+ act.sa_handler = SIG_DFL;
+ sigemptyset(&(act.sa_mask));
+ act.sa_flags = 0;
+ sigaction(SIGCHLD, &act, NULL);
+ }
+#else
+signal(SIGCHLD, SIG_DFL);
+#endif
+
+/* Make the forcing flag available for routers and transports, set up the
+global message id field, and initialize the count for returned files and the
+message size. This use of strcpy() is OK because the length id is checked when
+it is obtained from a command line (the -M or -q options), and otherwise it is
+known to be a valid message id. */
+
+if (id != message_id)
+ Ustrcpy(message_id, id);
+f.deliver_force = forced;
+return_count = 0;
+message_size = 0;
+
+/* Initialize some flags */
+
+update_spool = FALSE;
+remove_journal = TRUE;
+
+/* Set a known context for any ACLs we call via expansions */
+acl_where = ACL_WHERE_DELIVERY;
+
+/* Reset the random number generator, so that if several delivery processes are
+started from a queue runner that has already used random numbers (for sorting),
+they don't all get the same sequence. */
+
+random_seed = 0;
+
+/* Open and lock the message's data file. Exim locks on this one because the
+header file may get replaced as it is re-written during the delivery process.
+Any failures cause messages to be written to the log, except for missing files
+while queue running - another process probably completed delivery. As part of
+opening the data file, message_subdir gets set. */
+
+if ((deliver_datafile = spool_open_datafile(id)) < 0)
+ return continue_closedown(); /* yields DELIVER_NOT_ATTEMPTED */
+
+/* tHe value of message_size at this point has been set to the data length,
+plus one for the blank line that notionally precedes the data. */
+
+/* Now read the contents of the header file, which will set up the headers in
+store, and also the list of recipients and the tree of non-recipients and
+assorted flags. It updates message_size. If there is a reading or format error,
+give up; if the message has been around for sufficiently long, remove it. */
+
+ {
+ uschar * spoolname = string_sprintf("%s-H", id);
+ if ((rc = spool_read_header(spoolname, TRUE, TRUE)) != spool_read_OK)
+ {
+ if (errno == ERRNO_SPOOLFORMAT)
+ {
+ struct stat statbuf;
+ if (Ustat(spool_fname(US"input", message_subdir, spoolname, US""),
+ &statbuf) == 0)
+ log_write(0, LOG_MAIN, "Format error in spool file %s: "
+ "size=" OFF_T_FMT, spoolname, statbuf.st_size);
+ else
+ log_write(0, LOG_MAIN, "Format error in spool file %s", spoolname);
+ }
+ else
+ log_write(0, LOG_MAIN, "Error reading spool file %s: %s", spoolname,
+ strerror(errno));
+
+ /* If we managed to read the envelope data, received_time contains the
+ time the message was received. Otherwise, we can calculate it from the
+ message id. */
+
+ if (rc != spool_read_hdrerror)
+ {
+ received_time.tv_sec = received_time.tv_usec = 0;
+ /*III subsec precision?*/
+ for (i = 0; i < MESSAGE_ID_TIME_LEN; i++)
+ received_time.tv_sec = received_time.tv_sec * BASE_62 + tab62[id[i] - '0'];
+ }
+
+ /* If we've had this malformed message too long, sling it. */
+
+ if (now - received_time.tv_sec > keep_malformed)
+ {
+ Uunlink(spool_fname(US"msglog", message_subdir, id, US""));
+ Uunlink(spool_fname(US"input", message_subdir, id, US"-D"));
+ Uunlink(spool_fname(US"input", message_subdir, id, US"-H"));
+ Uunlink(spool_fname(US"input", message_subdir, id, US"-J"));
+ log_write(0, LOG_MAIN, "Message removed because older than %s",
+ readconf_printtime(keep_malformed));
+ }
+
+ (void)close(deliver_datafile);
+ deliver_datafile = -1;
+ return continue_closedown(); /* yields DELIVER_NOT_ATTEMPTED */
+ }
+ }
+
+/* The spool header file has been read. Look to see if there is an existing
+journal file for this message. If there is, it means that a previous delivery
+attempt crashed (program or host) before it could update the spool header file.
+Read the list of delivered addresses from the journal and add them to the
+nonrecipients tree. Then update the spool file. We can leave the journal in
+existence, as it will get further successful deliveries added to it in this
+run, and it will be deleted if this function gets to its end successfully.
+Otherwise it might be needed again. */
+
+ {
+ uschar * fname = spool_fname(US"input", message_subdir, id, US"-J");
+ FILE * jread;
+
+ if ( (journal_fd = Uopen(fname,
+ O_RDWR|O_APPEND | EXIM_CLOEXEC | EXIM_NOFOLLOW, SPOOL_MODE)) >= 0
+ && lseek(journal_fd, 0, SEEK_SET) == 0
+ && (jread = fdopen(journal_fd, "rb"))
+ )
+ {
+ while (Ufgets(big_buffer, big_buffer_size, jread))
+ {
+ int n = Ustrlen(big_buffer);
+ big_buffer[n-1] = 0;
+ tree_add_nonrecipient(big_buffer);
+ DEBUG(D_deliver) debug_printf("Previously delivered address %s taken from "
+ "journal file\n", big_buffer);
+ }
+ rewind(jread);
+ if ((journal_fd = dup(fileno(jread))) < 0)
+ journal_fd = fileno(jread);
+ else
+ (void) fclose(jread); /* Try to not leak the FILE resource */
+
+ /* Panic-dies on error */
+ (void)spool_write_header(message_id, SW_DELIVERING, NULL);
+ }
+ else if (errno != ENOENT)
+ {
+ log_write(0, LOG_MAIN|LOG_PANIC, "attempt to open journal for reading gave: "
+ "%s", strerror(errno));
+ return continue_closedown(); /* yields DELIVER_NOT_ATTEMPTED */
+ }
+
+ /* A null recipients list indicates some kind of disaster. */
+
+ if (!recipients_list)
+ {
+ (void)close(deliver_datafile);
+ deliver_datafile = -1;
+ log_write(0, LOG_MAIN, "Spool error: no recipients for %s", fname);
+ return continue_closedown(); /* yields DELIVER_NOT_ATTEMPTED */
+ }
+ }
+
+
+/* Handle a message that is frozen. There are a number of different things that
+can happen, but in the default situation, unless forced, no delivery is
+attempted. */
+
+if (f.deliver_freeze)
+ {
+#ifdef SUPPORT_MOVE_FROZEN_MESSAGES
+ /* Moving to another directory removes the message from Exim's view. Other
+ tools must be used to deal with it. Logging of this action happens in
+ spool_move_message() and its subfunctions. */
+
+ if ( move_frozen_messages
+ && spool_move_message(id, message_subdir, US"", US"F")
+ )
+ return continue_closedown(); /* yields DELIVER_NOT_ATTEMPTED */
+#endif
+
+ /* For all frozen messages (bounces or not), timeout_frozen_after sets the
+ maximum time to keep messages that are frozen. Thaw if we reach it, with a
+ flag causing all recipients to be failed. The time is the age of the
+ message, not the time since freezing. */
+
+ if (timeout_frozen_after > 0 && message_age >= timeout_frozen_after)
+ {
+ log_write(0, LOG_MAIN, "cancelled by timeout_frozen_after");
+ process_recipients = RECIP_FAIL_TIMEOUT;
+ }
+
+ /* For bounce messages (and others with no sender), thaw if the error message
+ ignore timer is exceeded. The message will be discarded if this delivery
+ fails. */
+
+ else if (!*sender_address && message_age >= ignore_bounce_errors_after)
+ log_write(0, LOG_MAIN, "Unfrozen by errmsg timer");
+
+ /* If this is a bounce message, or there's no auto thaw, or we haven't
+ reached the auto thaw time yet, and this delivery is not forced by an admin
+ user, do not attempt delivery of this message. Note that forced is set for
+ continuing messages down the same channel, in order to skip load checking and
+ ignore hold domains, but we don't want unfreezing in that case. */
+
+ else
+ {
+ if ( ( sender_address[0] == 0
+ || auto_thaw <= 0
+ || now <= deliver_frozen_at + auto_thaw
+ )
+ && ( !forced || !f.deliver_force_thaw
+ || !f.admin_user || continue_hostname
+ ) )
+ {
+ (void)close(deliver_datafile);
+ deliver_datafile = -1;
+ log_write(L_skip_delivery, LOG_MAIN, "Message is frozen");
+ return continue_closedown(); /* yields DELIVER_NOT_ATTEMPTED */
+ }
+
+ /* If delivery was forced (by an admin user), assume a manual thaw.
+ Otherwise it's an auto thaw. */
+
+ if (forced)
+ {
+ f.deliver_manual_thaw = TRUE;
+ log_write(0, LOG_MAIN, "Unfrozen by forced delivery");
+ }
+ else log_write(0, LOG_MAIN, "Unfrozen by auto-thaw");
+ }
+
+ /* We get here if any of the rules for unfreezing have triggered. */
+
+ f.deliver_freeze = FALSE;
+ update_spool = TRUE;
+ }
+
+
+/* Open the message log file if we are using them. This records details of
+deliveries, deferments, and failures for the benefit of the mail administrator.
+The log is not used by exim itself to track the progress of a message; that is
+done by rewriting the header spool file. */
+
+if (message_logs)
+ {
+ uschar * fname = spool_fname(US"msglog", message_subdir, id, US"");
+ uschar * error;
+ int fd;
+
+ if ((fd = open_msglog_file(fname, SPOOL_MODE, &error)) < 0)
+ {
+ log_write(0, LOG_MAIN|LOG_PANIC, "Couldn't %s message log %s: %s", error,
+ fname, strerror(errno));
+ return continue_closedown(); /* yields DELIVER_NOT_ATTEMPTED */
+ }
+
+ /* Make a stdio stream out of it. */
+
+ if (!(message_log = fdopen(fd, "a")))
+ {
+ log_write(0, LOG_MAIN|LOG_PANIC, "Couldn't fdopen message log %s: %s",
+ fname, strerror(errno));
+ return continue_closedown(); /* yields DELIVER_NOT_ATTEMPTED */
+ }
+ }
+
+
+/* If asked to give up on a message, log who did it, and set the action for all
+the addresses. */
+
+if (give_up)
+ {
+ struct passwd *pw = getpwuid(real_uid);
+ log_write(0, LOG_MAIN, "cancelled by %s",
+ pw ? US pw->pw_name : string_sprintf("uid %ld", (long int)real_uid));
+ process_recipients = RECIP_FAIL;
+ }
+
+/* Otherwise, if there are too many Received: headers, fail all recipients. */
+
+else if (received_count > received_headers_max)
+ process_recipients = RECIP_FAIL_LOOP;
+
+/* Otherwise, if a system-wide, address-independent message filter is
+specified, run it now, except in the case when we are failing all recipients as
+a result of timeout_frozen_after. If the system filter yields "delivered", then
+ignore the true recipients of the message. Failure of the filter file is
+logged, and the delivery attempt fails. */
+
+else if (system_filter && process_recipients != RECIP_FAIL_TIMEOUT)
+ {
+ int rc;
+ int filtertype;
+ ugid_block ugid;
+ redirect_block redirect;
+
+ if (system_filter_uid_set)
+ {
+ ugid.uid = system_filter_uid;
+ ugid.gid = system_filter_gid;
+ ugid.uid_set = ugid.gid_set = TRUE;
+ }
+ else
+ ugid.uid_set = ugid.gid_set = FALSE;
+
+ return_path = sender_address;
+ f.enable_dollar_recipients = TRUE; /* Permit $recipients in system filter */
+ f.system_filtering = TRUE;
+
+ /* Any error in the filter file causes a delivery to be abandoned. */
+
+ redirect.string = system_filter;
+ redirect.isfile = TRUE;
+ redirect.check_owner = redirect.check_group = FALSE;
+ redirect.owners = NULL;
+ redirect.owngroups = NULL;
+ redirect.pw = NULL;
+ redirect.modemask = 0;
+
+ DEBUG(D_deliver|D_filter) debug_printf("running system filter\n");
+
+ rc = rda_interpret(
+ &redirect, /* Where the data is */
+ RDO_DEFER | /* Turn on all the enabling options */
+ RDO_FAIL | /* Leave off all the disabling options */
+ RDO_FILTER |
+ RDO_FREEZE |
+ RDO_REALLOG |
+ RDO_REWRITE,
+ NULL, /* No :include: restriction (not used in filter) */
+ NULL, /* No sieve vacation directory (not sieve!) */
+ NULL, /* No sieve enotify mailto owner (not sieve!) */
+ NULL, /* No sieve user address (not sieve!) */
+ NULL, /* No sieve subaddress (not sieve!) */
+ &ugid, /* uid/gid data */
+ &addr_new, /* Where to hang generated addresses */
+ &filter_message, /* Where to put error message */
+ NULL, /* Don't skip syntax errors */
+ &filtertype, /* Will always be set to FILTER_EXIM for this call */
+ US"system filter"); /* For error messages */
+
+ DEBUG(D_deliver|D_filter) debug_printf("system filter returned %d\n", rc);
+
+ if (rc == FF_ERROR || rc == FF_NONEXIST)
+ {
+ (void)close(deliver_datafile);
+ deliver_datafile = -1;
+ log_write(0, LOG_MAIN|LOG_PANIC, "Error in system filter: %s",
+ string_printing(filter_message));
+ return continue_closedown(); /* yields DELIVER_NOT_ATTEMPTED */
+ }
+
+ /* Reset things. If the filter message is an empty string, which can happen
+ for a filter "fail" or "freeze" command with no text, reset it to NULL. */
+
+ f.system_filtering = FALSE;
+ f.enable_dollar_recipients = FALSE;
+ if (filter_message && filter_message[0] == 0) filter_message = NULL;
+
+ /* Save the values of the system filter variables so that user filters
+ can use them. */
+
+ memcpy(filter_sn, filter_n, sizeof(filter_sn));
+
+ /* The filter can request that delivery of the original addresses be
+ deferred. */
+
+ if (rc == FF_DEFER)
+ {
+ process_recipients = RECIP_DEFER;
+ deliver_msglog("Delivery deferred by system filter\n");
+ log_write(0, LOG_MAIN, "Delivery deferred by system filter");
+ }
+
+ /* The filter can request that a message be frozen, but this does not
+ take place if the message has been manually thawed. In that case, we must
+ unset "delivered", which is forced by the "freeze" command to make -bF
+ work properly. */