/*************************************************
*     Exim - an Internet mail transport agent    *
*************************************************/

/* Copyright (c) University of Cambridge 1995 - 1999 */
/* See the file NOTICE for conditions of use and distribution. */

/* Functions that operate on the input queue. */


#include "exim.h"



/*************************************************
*              Perform a queue run               *
*************************************************/

/*The arguments give the messages to start and stop at; NULL means start at the
beginning or stop at the end. If the given start message doesn't exist, we
start at the next lexically greater one, and likewise we stop at the after the
previous lexically lesser one if the given stop message doesn't exist. Because
a queue run can take some time, stat each file before forking, in case it has
been delivered in the meantime by some other means.

The global variables queue_force and queue_local may be set to cause forced
deliveries or local-only deliveries, respectively.

If deliver_selectstring is not NULL, skip messages whose recipients do not
contain the string. As this option is typically used when a machine comes back
online, we want to ensure that at least one delivery attempt takes place, so
force the first one.

If queue_2stage is set, the queue is scanned twice. The first time, queue_smtp
is set so that routing is done for all messages. Thus in the second run those
that are routed to the same host should go down the same SMTP connection.

Arguments:
  start_id   message id to start at, or NULL for all
  stop_id    message id to end at, or NULL for all
  recurse    TRUE if recursing for 2-stage run

Returns:     nothing
*/

void
queue_run(char *start_id, char *stop_id, BOOL recurse)
{
char *procfile;
BOOL force_delivery = queue_run_force || deliver_selectstring != NULL;
queue_filename *f;

/* Turn off the flags that cause SMTP deliveries and remote processing not to
happen, unless doing a 2-stage queue run, when the SMTP flag gets set. Save
the queue_runner's pid and the flag that indicates any deliveries run directly
from this process. Deliveries that are run by handing on TCP/IP channels have
queue_run_pid set, but not queue_running. Set up the name of the file for the
list of such processes that we have to wait for, and ensure that it does not
exist at the start. The only reason it might is that something crashed last
time a queue runner with this pid was running. Unlikely, but... */

queue_remote = FALSE;
queue_smtp = queue_2stage;
queue_run_pid = getpid();
queue_running = TRUE;

procfile = string_sprintf("%s/qr%d", spool_directory, queue_run_pid);
unlink(procfile);

/* Log the true start of a queue run */

if (!recurse)
  log_write(0, LOG_MAIN, "Start queue run: pid=%d%s%s", queue_run_pid,
    (deliver_selectstring == NULL)? "" : " -R ",
    (deliver_selectstring == NULL)? "" : deliver_selectstring);

/* Run the queue. If a start or finish id is given, we must take the queue in
its natural order. Otherwise "randomize" it so we don't always do things in the
same order. If deliver_selectstring is set, check for an undelivered address
that matches the string. This does mean we have to read the header file, and
then again when actually delivering, but it's cheaper than forking a delivery
process for each file. */

for (f = queue_get_spool_list(!queue_run_in_order &&
       start_id == NULL && stop_id == NULL);
     f != NULL; f = f->next)
  {
  if (stop_id != NULL && strcmp(f->text, stop_id) > 0) break;
  if (start_id == NULL || strcmp(f->text, start_id) >= 0)
    {
    pid_t pid;
    int status;
    struct stat statbuf;
    char buffer[256];

    message_subdir[0] = f->dir_char;
    sprintf(buffer, "%s/input/%s/%s", spool_directory, message_subdir, f->text);
    if (stat(buffer, &statbuf) < 0) continue;

    /* Check for a matching address if deliver_selectstring is set. If so,
    we do a fully delivery - don't want to omit other addresses since
    their routing might trigger re-writing etc. */

    if (deliver_selectstring != NULL)
      {
      BOOL found = FALSE;
      void *reset_point = store_get(0);
      if (spool_read_header(f->text, FALSE, TRUE) != spool_read_OK) continue;

      if (recipients_list != NULL)
        {
        int i;
        for (i = 0; i < recipients_count; i++)
          {
          if (
            strstric(recipients_list[i].address, deliver_selectstring, FALSE)
              != NULL &&
                tree_search_addr(tree_nonrecipients, recipients_list[i].address,
                  FALSE) == NULL)
            {
            found = TRUE;
            break;
            }
          }
        }

      store_reset(reset_point);

      /* If no addresses matched the string, skip this message. */

      if (!found)
        {
        DEBUG(9) debug_printf("%s: no addresses matched %s\n",
          f->text, deliver_selectstring);
        continue;
        }
      }

    /* OK, got a message we want to deliver; cut the -H off the name first */

    set_process_info("running queue: %s", f->text);
    f->text[SPOOL_NAME_LENGTH-2] = 0;
    if ((pid = fork()) == 0)
      _exit(deliver_message(f->text, force_delivery, queue_run_local, FALSE));

    if (pid < 0)
      log_write(0, LOG_PANIC_DIE, "fork of delivery process failed\n");

    set_process_info("running queue: waiting for %s (%d)", f->text, pid);
    while (wait(&status) != pid);

    /* A successful return means a delivery was attempted; turn off the
    force flag for any subsequent calls unless queue_force is set. */

    if ((status & 0xffff) == 0) force_delivery = queue_run_force;

    /* Otherwise, if the delivery yielded DEFER it means the load average
    was too high to attempt a delivery. Abandon the queue run. A message
    has already been written to the log by the delivery process. If the
    delivery yielded PASS, it means it passed on one or more TCP/IP connections
    to other processes. Their pids are written into a file on the spool, and
    the queue-runner should wait for them to complete before proceeding. */

    if ((status & 0x00ff) == 0)
      {
      int rc = (status & 0xff00) >> 8;
      if (rc == DEFER) break;

      /* The delivery passed on one or more TCP/IP connections. Read the
      sub-process pids from the file, and wait for them. The only way we can do
      this is by sending SIGHUP (which will be ignored) and waiting when the
      kill() succeeds, implying that the process exists. We assume that, since
      the channel is set up, delivery down it should be quite fast, so just
      wait 1 second, but ensure we don't wait for ever. If anything goes
      wrong, just log and continue with the queue runner. When we get to the
      end of the file, all the descendents of the original delivery should be
      finished. */

      if (rc == PASS)
        {
        int fd = open(procfile, O_RDONLY);
        if (fd < 0)
          {
          log_write(0, LOG_MAIN|LOG_PANIC, "queue_run: failed to open %s: %s",
            procfile, strerror(errno));
          }
        else
          {
          for(;;)
            {
            int i;
            pid_t child_pid;

            if (read(fd, &child_pid, sizeof(pid_t)) != sizeof(pid_t)) break;

            set_process_info("running queue: waiting for descendent %d",
              child_pid);

            for (i = 1; i < 3600; i++)
              {
              if (kill(child_pid, SIGHUP) < 0) break;
              sleep(1);
              }
            }

          /* Close and get rid of the file, ready for re-use on a subsequent
          delivery. */

          unlink(procfile);
          close(fd);
          }
        }
      }

    /* If the process crashed, tell somebody */

    if ((status & 0x00ff) != 0)
      log_write(0, LOG_MAIN|LOG_PANIC,
        "queue run: process %d crashed with signal %d while delivering %s",
        (int)pid, status & 0x00ff, f->text);

    set_process_info("running queue");
    }
  }

/* If queue_2stage is true, we do it all again, with the 2stage flag
turned off. */

if (queue_2stage)
  {
  queue_2stage = FALSE;
  queue_run(start_id, stop_id, TRUE);
  }

/* At top level, if smtp_etrn_serialize is set, remove the serialization
block for this -R text, if any. Then log the end of the run. */

if (!recurse)
  {
  if (smtp_etrn_serialize && deliver_selectstring != NULL)
    transport_end_serialized("etrn-runs", deliver_selectstring);
  log_write(0, LOG_MAIN, "End queue run: pid=%d%s%s", getpid(),
    (deliver_selectstring == NULL)? "" : " -R ",
    (deliver_selectstring == NULL)? "" : deliver_selectstring);
  }
}



/*************************************************
*             Get list of spool files            *
*************************************************/

/* Scan the spool directory and return a list of the relevant file names
therein. If the argument is TRUE, they are returned in "randomized" order.
Actually, the order is anything but random, but the algorithm is cheap, and
the point is simply to ensure that the same order doesn't occur every time, in
case a particular message is causing a remote MTA to barf - we would like to
try other messages to that MTA first. If the argument is FALSE, sort the list
according to the file name. This should give the order in which the messages
arrived. It is normally used only for presentation to humans, in which case the
insertion sort that it does is not part of the normal operational code.
However, if the queue_run_in order is set, or if a -q option specifies starting
and stopping messages, the sort is required.

If any single-character sub-directories are encountered in the initial
scan, they are subsequently scanned for message files too. The returned
data blocks contain the identifying character of the subdirectory, if any.

Argument:   TRUE if the order of the list is to be unpredictable
Returns:    pointer to a chain of queue name items
*/

queue_filename *
queue_get_spool_list(BOOL randomize)
{
int i;
int subdir_max = 1;
int flags = 0;
int resetflags = -1;
int subptr;
queue_filename *yield = NULL;
queue_filename *last = NULL;
struct dirent *ent;
DIR *dd;
char subdirs[64];
char buffer[256];

/* The file names are added onto the start or end of the list according to the
bits of the flags variable. When randomizing, get a collection of bits from the
current time. Use the bottom 16 and just keep re-using them if necessary. */

if (randomize) resetflags = time(NULL) & 0xFFFF;

/* This loop runs at least once, for the main directory, and then as many
times as necessary to scan any subdirectories encountered in the main
directory. */

subdirs[0] = 0;
sprintf(buffer, "%s/input", spool_directory);
subptr = (int)strlen(buffer);
buffer[subptr+2] = 0;               /* terminator for lengthened name */

for (i = 0; i < subdir_max; i++)
  {
  int count = 0;
  int subdirchar = subdirs[i];      /* 0 for main directory */
  if (subdirchar != 0)
    {
    buffer[subptr] = '/';
    buffer[subptr+1] = subdirchar;
    }
  dd = opendir(buffer);
  if (dd == NULL) continue;

  /* Now scan the directory. */

  while ((ent = readdir(dd)) != NULL)
    {
    char *name = ent->d_name;
    int len = (int)strlen(name);

    /* Count entries */

    count++;

    /* If we find a single alphameric sub-directory on the first
    pass, add it to the list for subsequent scans. */

    if (i == 0 && len == 1 && isalnum(*name))
      {
      subdirs[subdir_max++] = *name;
      continue;
      }

    /* Otherwise, if it is a header spool file, add it to the list */

    if (len == SPOOL_NAME_LENGTH &&
        strcmp(name + SPOOL_NAME_LENGTH - 2, "-H") == 0)
      {
      queue_filename *next =
        store_get(sizeof(queue_filename) + (int)strlen(name));
      strcpy(next->text, name);
      next->dir_char = subdirchar;

      /* First item becomes the top and bottom of the list. */

      if (yield == NULL)
        {
        next->next = NULL;
        yield = last = next;
        }

      /* If randomizing, insert at either top or bottom, randomly. This is, I
      argue, faster than doing a sort by allocating a random number to each item,
      and it also saves having to store the number with each item. */

      else if (randomize)
        {
        if (flags == 0) flags = resetflags;
        if ((flags & 1) == 0)
          {
          next->next = yield;
          yield = next;
          }
        else
          {
          next->next = NULL;
          last->next = next;
          last = next;
          }
        flags = flags >> 1;
        }

      /* Otherwise do an insertion sort based on the name. First see if
      it should go before the first item. */

      else if (strcmp(next->text, yield->text) < 0)
        {
        next->next = yield;
        yield = next;
        }

      /* Otherwise find the item it should go after; check the last one
      first, because that will often be the case. */

      else
        {
        queue_filename *this;
        if (strcmp(next->text, last->text) < 0)
          {
          for (this = yield; this != last; this = this->next)
            if (strcmp(next->text, this->next->text) < 0) break;
          }
        else this = last;
        next->next = this->next;
        this->next = next;
        if (this == last) last = next;
        }
      }
    }

  /* Close the directory and, it was empty (count == 2 implies just "." and
  ".." entries) and it was a subdirectory, and Exim is no longer configured
  to use subdirectories, attempt to get rid of it. At the same time, try
  to get rid of any corresponding msglog subdirectory. These are just
  cosmetic tidying actions, so just ignore failures. */

  closedir(dd);
  if (i != 0 && !split_spool_directory && count <= 2)
    {
    rmdir(buffer);
    sprintf(big_buffer, "%s/msglog/%c", spool_directory, subdirchar);
    rmdir(big_buffer);
    }
  }

return yield;
}




/************************************************
*          List extra deliveries                *
************************************************/

/* This is called from queue_list below to print out all addresses that
have received a message but which were not primary addresses. That is, all
the addresses in the tree of non-recipients that are not primary addresses.
The tree has been scanned and the data field filled in for those that are
primary addresses.

Argument:    points to the tree node
Returns:     nothing
*/

static void queue_list_extras(tree_node *p)
{
if (p->left != NULL) queue_list_extras(p->left);
if (!p->data.val) printf("       +D %s\n", p->name);
if (p->right != NULL) queue_list_extras(p->right);
}



/************************************************
*          List messages on the queue           *
************************************************/

/* Or a given list of messages. In the "all" case, we get a list of file names
as quickly as possible, then scan each one for information to output. If any
disappear while we are processing, just leave them out, but give an error if an
explicit list was given. This function is a top-level function that is obeyed
as a result of the -bp argument. As there may be a lot of messages on the
queue, we must tidy up the store after reading the headers for each one.

Arguments:
   option     0 => list top-level recipients, with "D" for those delivered
              1 => list only undelivered top-level recipients
              2 => as 0, plus any generated delivered recipients
              If 8 is added to any of these values, the queue is listed in
                random order.
   list       => first of any message ids to list
   count      count of message ids; 0 => all

Returns:      nothing
*/

void
queue_list(int option, char **list, int count)
{
int i;
int now = (int)time(NULL);
void *reset_point;
queue_filename *f;

/* If given a list of messages, build a chain containing their ids. */

if (count > 0)
  {
  queue_filename *last;
  for (i = 0; i < count; i++)
    {
    queue_filename *next =
      store_get(sizeof(queue_filename) + (int)strlen(list[i]) + 2);
    sprintf(next->text, "%s-H", list[i]);
    next->dir_char = '*';
    next->next = NULL;
    if (i == 0) f = next; else last->next = next;
    last = next;
    }
  }

/* Otherwise get a list of the entire queue, in order if necessary. */

else f = queue_get_spool_list(option >= 8);

if (option >= 8) option -= 8;

/* Now scan the chain and print information, resetting store used
each time. If queue_list_requires_admin is TRUE and the caller is not
an admin user, list only those messages that were submitted by the caller. */

reset_point = store_get(0);

for (; f != NULL; f = f->next)
  {
  int rc, save_errno;
  int size = 0;
  BOOL env_read;

  store_reset(reset_point);
  message_size = 0;
  message_subdir[0] = f->dir_char;
  rc = spool_read_header(f->text, FALSE, count <= 0);
  if (rc == spool_read_notopen && errno == ENOENT && count <= 0) continue;
  save_errno = errno;

  if (queue_list_requires_admin && !admin_user &&
     (rc != spool_read_OK || real_uid != originator_uid)) continue;

  env_read = (rc == spool_read_OK || rc == spool_read_hdrerror);

  if (env_read)
    {
    int ptr;
    FILE *jread;
    struct stat statbuf;

    sprintf(big_buffer, "%s/input/%s/%s", spool_directory, message_subdir,
      f->text);
    ptr = (int)strlen(big_buffer)-1;
    big_buffer[ptr] = 'D';

    if (stat(big_buffer, &statbuf) == 0) size = message_size + statbuf.st_size;
    i = (now - received_time)/60;  /* minutes on queue */
    if (i > 90)
      {
      i = (i + 30)/60;
      if (i > 72) printf("%2dd ", (i + 12)/24); else printf("%2dh ", i);
      }
    else printf("%2dm ", i);

    /* Collect delivered addresses from any J file */

    big_buffer[ptr] = 'J';
    jread = fopen(big_buffer, "r");
    if (jread != NULL)
      {
      while (fgets(big_buffer, big_buffer_size, jread) != NULL)
        {
        int n = (int)strlen(big_buffer);
        big_buffer[n-1] = 0;
        tree_add_nonrecipient(big_buffer+1, big_buffer[0] == 'Y');
        }
      fclose(jread);
      }
    }

  fprintf(stdout, "%s ", string_format_size(size, big_buffer));
  for (i = 0; i < 16; i++) fputc(f->text[i], stdout);

  if (env_read && sender_address != NULL)
    {
    if (user_null_sender && sender_address[0] != 0)
      printf(" <> (%s)", sender_address);
    else printf(" <%s>", sender_address);
    }

  if (rc != spool_read_OK)
    {
    printf("\n    ");
    if (save_errno == ERRNO_SPOOLFORMAT)
      {
      struct stat statbuf;
      sprintf(big_buffer, "%s/input/%s/%s", spool_directory, message_subdir,
        f->text);
      if (stat(big_buffer, &statbuf) == 0)
        {
        int size = statbuf.st_size;    /* Because might be a long */
        printf("*** spool format error: size=%d ***", size);
        }
      else printf("*** spool format error ***");
      }
    else printf("*** spool read error: %s ***", strerror(save_errno));
    if (rc != spool_read_hdrerror)
      {
      printf("\n\n");
      continue;
      }
    }

  if (deliver_freeze) printf(" *** frozen ***");

  printf("\n");

  if (recipients_list != NULL)
    {
    for (i = 0; i < recipients_count; i++)
      {
      tree_node *delivered =
        tree_search_addr(tree_nonrecipients, recipients_list[i].address, FALSE);
      if (!delivered || option != 1)
        printf("        %s %s\n", (delivered != NULL)? "D":" ",
          recipients_list[i].address);
      if (delivered != NULL) delivered->data.val = TRUE;
      }
    if (option == 2 && tree_nonrecipients != NULL)
      queue_list_extras(tree_nonrecipients);
    printf("\n");
    }
  }
}



/*************************************************
*             Act on a specific message          *
*************************************************/

/* Actions that require a list of addresses make use of
argv/argc/recipients_arg. Other actions do not. This function does its
own authority checking.

Arguments:
  id              id of the message to work on
  action          which action is required (MSG_xxx)
  argv            the original argv for Exim
  argc            the original argc for Exim
  recipients_arg  offset to the list of recipients in argv

Returns:          FALSE if there was any problem
*/

BOOL
queue_action(char *id, int action, char **argv, int argc, int recipients_arg)
{
int i, j;
BOOL yield = TRUE;
struct passwd *pw;
char *doing = NULL;
char *username;
char spoolname[256];

/* Set the global message_id variable, used when re-writing spool files. This
also causes message ids to be added to log messages. */

strcpy(message_id, id);

/* The "actions" that just list the files do not require any locking to be
done. Only admin users may read the spool files. */

if (action >= MSG_SHOW_BODY)
  {
  int fd, i, rc;
  char *subdirectory, *suffix;

  if (!admin_user)
    {
    printf("Permission denied\n");
    return FALSE;
    }

  if (recipients_arg < argc)
    {
    printf("*** Only one message can be listed at once\n");
    return FALSE;
    }

  if (action == MSG_SHOW_BODY)
    {
    subdirectory = "input";
    suffix = "-D";
    }
  else if (action == MSG_SHOW_HEADER)
    {
    subdirectory = "input";
    suffix = "-H";
    }
  else
    {
    subdirectory = "msglog";
    suffix = "";
    }

  for (i = 0; i < 2; i++)
    {
    message_subdir[0] = (split_spool_directory == (i == 0))? id[5] : 0;
    sprintf(spoolname, "%s/%s/%s/%s%s", spool_directory, subdirectory,
      message_subdir, id, suffix);
    fd = open(spoolname, O_RDONLY);
    if (fd >= 0) break;
    if (i == 0) continue;
    printf("Failed to open %s file for %s%s: %s\n", subdirectory, id, suffix,
      strerror(errno));
    return FALSE;
    }

  while((rc = read(fd, big_buffer, big_buffer_size)) > 0)
    write(fileno(stdout), big_buffer, rc);

  return TRUE;
  }

/* For actions that actually act, open and lock the data file to ensure that no
other process is working on this message. If the file does not exist, continue
only if the action is remove and the user is an admin user, to allow for
tidying up broken states. */

if (!spool_open_datafile(id))
  {
  if (errno == ENOENT)
    {
    yield = FALSE;
    printf("Spool data file for %s does not exist\n", id);
    if (action != MSG_REMOVE || !admin_user) return FALSE;
    printf("Continuing, to ensure all files removed\n");
    }
  else
    {
    if (errno == 0) printf("Message %s is locked\n", id);
      else printf("Couldn't open spool file for %s: %s\n", id,
        strerror(errno));
    return FALSE;
    }
  }

/* Read the spool header file for the message. Again, continue after an
error only in the case of deleting by an administrator. Setting the third
argument false causes it to look both in the main spool directory and in
the appropriate subdirectory, and set message_subdir according to where it
found the message. */

sprintf(spoolname, "%s-H", id);
if (spool_read_header(spoolname, TRUE, FALSE) != spool_read_OK)
  {
  yield = FALSE;
  if (errno != ERRNO_SPOOLFORMAT)
    printf("Spool read error for %s: %s\n", spoolname, strerror(errno));
  else
    printf("Spool format error for %s\n", spoolname);
  if (action != MSG_REMOVE || !admin_user)
    {
    close(deliver_datafile);
    deliver_datafile = -1;
    return FALSE;
    }
  printf("Continuing to ensure all files removed\n");
  }

/* Check that the user running this process is entitled to operate on this
message. Only admin users may freeze/thaw, add/cancel recipients, or otherwise
mess about, but the original sender is permitted to remove a message. That's
why we leave this check until after the headers are read. */

if (!admin_user && (action != MSG_REMOVE || real_uid != originator_uid))
  {
  printf("Permission denied\n");
  close(deliver_datafile);
  deliver_datafile = -1;
  return FALSE;
  }

/* Set up the user name for logging. */

pw = getpwuid(real_uid);
username = (pw != NULL)? pw->pw_name : string_sprintf("uid %d", (int)real_uid);

/* Take the necessary action. */

printf("Message %s ", id);

switch(action)
  {
  case MSG_FREEZE:
  if (deliver_freeze)
    {
    yield = FALSE;
    printf("is already frozen\n");
    }
  else
    {
    deliver_freeze = TRUE;
    deliver_manual_thaw = FALSE;
    deliver_frozen_at = time(NULL);
    if (spool_write_header(id) >= 0)
      {
      printf("is now frozen\n");
      log_write(0, LOG_MAIN, "frozen by %s", username);
      }
    else
      {
      yield = FALSE;
      printf("could not be frozen: failed to rewrite "
        "spool header file\n");
      log_write(0, LOG_MAIN, "failed to rewrite header file");
      }
    }
  break;


  case MSG_THAW:
  if (!deliver_freeze)
    {
    yield = FALSE;
    printf("is not frozen\n");
    }
  else
    {
    deliver_freeze = FALSE;
    deliver_manual_thaw = TRUE;
    if (spool_write_header(id) >= 0)
      {
      printf("is no longer frozen\n");
      log_write(0, LOG_MAIN, "unfrozen by %s", username);
      }
    else
      {
      yield = FALSE;
      printf("could not be unfrozen: failed to rewrite "
        "spool header file\n");
      log_write(0, LOG_MAIN, "failed to rewrite header file");
      }
    }
  break;


  /* We must ensure all files are removed from both the input directory
  and the appropriate subdirectory, to clean up cases when there are odd
  files left lying around in odd places. In the normal case message_subdir
  will have been set correctly by spool_read_header, but as this is a rare
  operation, just run everything twice. */

  case MSG_REMOVE:
  message_subdir[0] = id[5];
  for (j = 0; j < 2; message_subdir[0] = 0, j++)
    {
    sprintf(spoolname, "%s/msglog/%s/%s", spool_directory, message_subdir, id);
    if (unlink(spoolname) < 0)
      {
      if (errno != ENOENT)
        {
        yield = FALSE;
        printf("Error while removing %s: %s\n", spoolname,
          strerror(errno));
        }
      }

    for (i = 0; i < 3; i++)
      {
      sprintf(spoolname, "%s/input/%s/%s-%c", spool_directory, message_subdir,
        id, "DHJ"[i]);
      if (unlink(spoolname) < 0)
        {
        if (errno != ENOENT)
          {
          yield = FALSE;
          printf("Error while removing %s: %s\n", spoolname,
            strerror(errno));
          }
        }
      }
    }

  /* In the common case, the datafile is open (and locked), so give the
  obvious message. Otherwise be more specific. */

  if (deliver_datafile >= 0) printf("has been removed\n");
    else printf("has been removed or did not exist\n");
  log_write(0, LOG_MAIN, "removed by %s", username);
  break;


  case MSG_MARK_ALL_DELIVERED:
  for (i = 0; i < recipients_count; i++)
    {
    tree_add_nonrecipient(recipients_list[i].address, FALSE);
    }
  if (spool_write_header(id) >= 0)
    {
    printf("has been modified\n");
    for (i = 0; i < recipients_count; i++)
      log_write(0, LOG_MAIN, "address <%s> marked delivered by %s",
        recipients_list[i].address, username);
    }
  else
    {
    yield = FALSE;
    printf("- failed to rewrite spool header file "
      "while marking all recipients delivered\n");
    log_write(0, LOG_MAIN, "failed to rewrite header file while "
      "marking all recipients delivered");
    }
  break;


  case MSG_EDIT_SENDER:
  if (recipients_arg < argc - 1)
    {
    yield = FALSE;
    printf("- only one sender address can be specified\n");
    break;
    }
  doing = "editing sender";
  /* Fall through */

  case MSG_ADD_RECIPIENT:
  if (doing == NULL) doing = "adding recipient";
  /* Fall through */

  case MSG_MARK_DELIVERED:
  if (doing == NULL) doing = "marking as delivered";

  /* Common code for EDIT_SENDER, ADD_RECIPIENT, & MARK_DELIVERED */

  if (recipients_arg >= argc)
    {
    yield = FALSE;
    printf("- error while %s: no address given\n", doing);
    }
  else
    {
    for (; recipients_arg < argc; recipients_arg++)
      {
      int start, end, domain;
      char *errmess;
      char *receiver =
        parse_extract_address(argv[recipients_arg], &errmess, &start, &end,
          &domain, (action == MSG_EDIT_SENDER));

      if (receiver == NULL)
        {
        yield = FALSE;
        printf("- error while %s:\n  bad address %s: %s\n",
          doing, argv[recipients_arg], errmess);
        }
      else if (receiver[0] != 0 && domain == 0)
        {
        yield = FALSE;
        printf("- error while %s:\n  bad address %s: "
          "domain missing\n", doing, argv[recipients_arg]);
        }
      else
        {
        if (action == MSG_ADD_RECIPIENT)
          {
          accept_add_recipient(receiver, NULL, 0, 0);
          log_write(0, LOG_MAIN, "recipient <%s> added by %s",
            receiver, username);
          }
        else if (action == MSG_MARK_DELIVERED)
          {
          for (i = 0; i < recipients_count; i++)
            if (strcmp(recipients_list[i].address, receiver) == 0) break;
          if (i >= recipients_count)
            {
            printf("- error while %s:\n  %s is not a recipient:"
              " message not updated\n", doing, receiver);
            yield = FALSE;
            }
          else
            {
            tree_add_nonrecipient(receiver, FALSE);
            log_write(0, LOG_MAIN, "address <%s> marked delivered by %s",
              receiver, username);
            }
          }
        else  /* MSG_EDIT_SENDER */
          {
          sender_address = receiver;
          user_null_sender = FALSE;
          log_write(0, LOG_MAIN, "sender address changed to <%s> by %s",
            receiver, username);
          }
        }
      }


    if (yield)
      {
      if (spool_write_header(id) >= 0)
        printf("has been modified\n");
      else
        {
        yield = FALSE;
        printf("- failed to rewrite spool header file "
          "while %s\n", doing);
        log_write(0, LOG_MAIN, "failed to rewrite header file while %s",
          doing);
        }
      }
    }
  break;


  case MSG_EDIT_BODY:
  if (recipients_arg < argc)
    {
    yield = FALSE;
    printf("- only one message can be edited at once\n");
    }

  /* Make a copy of the body, and let the editor work on that. If the editor
  returns successfully, replace the body with the new text. This is the only
  way to allow the editor to do intermittent saves while preserving the right
  of the human to abort the whole thing. As soon as the rename() is done, the
  message becomes available for some other process to work on, since the new
  file is not locked, but that's OK because the next thing this process does is
  to close the old file anyway. */

  else
    {
    int copy_fd;
    int rc;
    char copyname[256];

    /* Make a temporary file to copy to */

    sprintf(copyname, "%s/input/%s/%s-D-%d", spool_directory, message_subdir,
      id, (int)getpid());
    copy_fd = open(copyname, O_WRONLY|O_CREAT, SPOOL_MODE);
    if (copy_fd < 0)
      {
      printf("not modified: opening copy file failed: %s",
        strerror(errno));
      yield = FALSE;
      break;
      }

    /* Make sure it has the same characteristics as the -D file */

    if (exim_uid_set)
      {
      fchown(copy_fd, exim_uid, exim_gid);
      fchmod(copy_fd, SPOOL_MODE);
      }

    /* Copy the contents of the -D file */

    while((rc = read(deliver_datafile, big_buffer, big_buffer_size)) > 0)
      {
      if (write(copy_fd, big_buffer, rc) != rc)
        {
        printf("not modified: copying failed (write): %s\n",
          strerror(errno));
        yield = FALSE;
        break;
        }
      }

    if (rc < 0)
      {
      printf("not modified: copying failed (read): %s\n",
        strerror(errno));
      yield = FALSE;
      }

    close(copy_fd);

    /* Now call the editor and act according to its yield */

    if (yield)
      {
      sprintf(spoolname, "/bin/sh -c \"${VISUAL:-${EDITOR:-vi}} %s\"",
        copyname);

      if ((system(spoolname) & 0xffff) == 0)
        {
        sprintf(spoolname, "%s/input/%s/%s-D", spool_directory, message_subdir,
          id);
        if ((rc = rename(copyname, spoolname)) == 0)
          {
          printf("has been modified\n");
          log_write(0, LOG_MAIN, "body edited by %s", username);
          }
        else
          {
          printf("not modified: rename failed: %s\n",
            strerror(errno));
          yield = FALSE;
          }
        }
      else
        {
        printf("not modified: editing failed\n");
        yield = FALSE;
        }
      }

    /* Get rid of the copy file if something went wrong */

    if (!yield) unlink(copyname);
    }
  break;
  }


/* Closing the datafile releases the lock and permits other processes
to operate on the message (if it still exists). */

close(deliver_datafile);
deliver_datafile = -1;
return yield;
}



/*************************************************
*         Check the queue_only condition         *
*************************************************/

/* The queue_only option forces messages to be queued; there is also an option
which sets certain kinds of queueing if a given file exists. For special kinds
of queueing, the relevant flags are set. If everthing is to be queued, return
TRUE.

Arguments:  none
Returns:    TRUE if queue_only is set or if the file existence requires
            unconditional queueing
*/

void
queue_check_only(void)
{
BOOL *set;
struct stat statbuf;
char *s, *ss, *name;
char buffer[1024];

if (queue_only_file == NULL) return;

s = queue_only_file;
for (ss = string_nextinlist(&s, ':', buffer, sizeof(buffer));
     ss != NULL;
     ss = string_nextinlist(&s, ':', buffer, sizeof(buffer)))
  {
  if (strncmp(ss, "remote", 6) == 0)
    {
    name = "queue_remote";
    set = &queue_remote;
    ss += 6;
    }
  else if (strncmp(ss, "smtp", 4) == 0)
    {
    name = "queue_smtp";
    set = &queue_smtp;
    ss += 4;
    }
  else
    {
    name = "queue_only";
    set = &queue_only;
    }

  if (stat(ss, &statbuf) == 0)
    {
    *set = TRUE;
    DEBUG(2) debug_printf("%s set because %s exists\n", name, ss);
    }
  }
}

/* End of queue.c */
