/*************************************************
*                      Smailmon                  *
*************************************************/

/* Copyright (c) University of Cambridge 1995. */

/* P. Hazel, February 1995*/
/* Last Modified: March 1995 */

#include "smailmon.h"


/* This module contains functions to do with scanning smail's
queue and displaying the data therefrom. */


/*************************************************
*                 Static variables               *
*************************************************/

static int input_mtime = 0;   /* mtime for input directory */
static int queue_total = 0;   /* number of items in queue */

/* Table for turning base-62 numbers into binary */

static char tab62[] =
          {0,1,2,3,4,5,6,7,8,9,0,0,0,0,0,0,     /* 0-9 */
           0,10,11,12,13,14,15,16,17,18,19,20,  /* A-K */
          21,22,23,24,25,26,27,28,29,30,31,32,  /* L-W */
          33,34,35, 0, 0, 0, 0, 0,              /* X-Z */
           0,36,37,38,39,40,41,42,43,44,45,46,  /* a-k */
          47,48,49,50,51,52,53,54,55,56,57,58,  /* l-w */
          59,60,61};                            /* x-z */
          
/* Index for quickly finding things in the ordered queue. */

static queue_item *queue_index[queue_index_size];



/*************************************************
*         Find/Create/Delete a destination       *
*************************************************/

/* If the action is dest_noop, then just return item or NULL;
if it is dest_add, then add if not present, and return item;
if it is dest_remove, remove if present and return NULL. The
address is lowercased to start with. */

dest_item *find_dest(queue_item *q, char *name, int action)
{
dest_item *dd;
dest_item **d = &(q->destinations);
char *s = name;

while (*s) { *s = tolower(*s); s++; }

while (*d != NULL)
  {
   
  if (strcmp(name, (*d)->address) == 0)
    {
    if (action != dest_remove) return *d; 
    dd = *d;
    *d = dd->next;
    free(dd);  
    return NULL;
    }
  d = &((*d)->next);     
  }
  
if (action != dest_add) return NULL;  

dd = (dest_item *)malloc(sizeof(dest_item) + (int)strlen(name));
strcpy(dd->address, name);
dd->next = NULL;
*d = dd;
return dd;
}



/*************************************************
*            Clean up a dead queue item          *
*************************************************/

static void clean_up(queue_item *p)
{
dest_item *dd = p->destinations;
while (dd != NULL)
  {
  dest_item *next = dd->next;
  free(dd);
  dd = next;   
  } 
if (p->sender != NULL) free(p->sender);
free(p);
}


/*************************************************
*             Set up new queue item              *
*************************************************/

static queue_item *set_up(char *name)
{
int i;
int t = 0;
int to_inside = FALSE;
queue_item *q = (queue_item *)malloc(sizeof(queue_item));
char *p;
char default_sender[32];

/* Initialize the block */

q->next = q->prev = NULL;
q->destinations = NULL;
q->msglog_time = 0;
strcpy(q->name, name);
q->seen = TRUE;
q->sender = NULL;

/* Compute the input time by interpreting the id as a base-62
number. We could stat the spool file, but its ctime is when
the message was finally completely received, and anyway,
files can be moved/copied from the error directory or even,
in extreme cases, be edited by hand. */

for (i = 0; i < 6; i++) t = t * 62 + tab62[name[i] - '0'];
q->input_time = t;

/* Attempt to read the spool file using the privileged readspool
program, if it is available. (The spool file may, of course,
have vanished by the time we come to look.) Readspool will have
checked the format of the file. If we can't run readspool, assume
is isn't available and give a TRUE return - the message just won't
have the spool file data displayed. If the first line is end of file,
the file is in the process of being written - or the call got dropped
before it could be written. Just do nothing. */

if (!read_spool_init(name)) goto SUCCESSEXIT;
if (!read_spool(queue_buffer)) goto SUCCESSEXIT;

/* If there was a problem, the first line will contain an error
message. If this is "permission denied", then readspool is
probably not setuid, and we can't get any info from it. If it
is "file not found", then the file has been deleted under our
feet. Return FALSE so that it does not get added to the display.
If the spool file is empty (not yet written) or malformed or
whatever, we just set up no data. */

if (strstr(queue_buffer, "readspool: permission denied")) goto SUCCESSEXIT;
if (strstr(queue_buffer, "readspool: file not found")) goto ERROREXIT;

/* Set default sender from user name - lines from readspool are
returned without a trailing '\n', i.e. already "chopped". */

p = queue_buffer + (int)strlen(queue_buffer) - 1;
while (p >= queue_buffer && *p == ' ') p--;
p[1] = 0;
sprintf(default_sender, "%-8s", queue_buffer+1);  /* make up to 8 chars */

/* If no sender got set, the spool file is probably in the process
of being written. Give an error return - we should pick up
the file again next time round. */

if (default_sender[0] == 0) goto ERROREXIT;

/* Skip second line (uid, gid) */

if (!read_spool(queue_buffer)) goto ERROREXIT;

/* Now scan arguments and destinations, keeping the sender and
all the destinations. */

while (read_spool(queue_buffer))
  {
  int c;

  if (queue_buffer[0] == 0) break;    /* end of envelope */

  /* The -f option specifies the sender */
   
  if (strcmp(queue_buffer, "!-f") == 0)
    {
    if (read_spool(queue_buffer))
      {
      if ((p = strstric(queue_buffer+1, visible_name, FALSE)) == NULL ||
        *(--p) != '@')
          p = queue_buffer + (int)strlen(queue_buffer);
      while (p - queue_buffer - 1 < 8) *p++ = ' ';
      *p = 0;
      q->sender = copystring(queue_buffer+1);
      }
    }

  /* The -t option requests taking destinations from inside */
   
  else if (strcmp(queue_buffer, "!-t") == 0) to_inside = TRUE;

  /* Various other options have a following argument line which
  we must ignore. */
    
  else if (strcmp(queue_buffer, "!-F") == 0 ||
    (strncmp(queue_buffer, "!-oM", 4) == 0 &&
      ((c = queue_buffer[4]) == 's' || c=='a'||c=='r'||c=='u'||c=='v'||c=='P')
        )) (void)read_spool(queue_buffer);  /* skip argument line */

  /* We have a destination. Add it to the chain of destinations if it
  is not already present. */
    
  else if (queue_buffer[0] != '!' || queue_buffer[1] != '-')
    {
    char *d = queue_buffer + 1;
    if (*d != '<')
      {
      char *s = d + (int)strlen(d);
      s[1] = '>';
      s[2] = 0;
      while (s > d) { s--; s[1] = s[0]; }
      *d = '<';    
      } 
    (void)find_dest(q, d, dest_add);
    }
  }

/* If we didn't see -f, use the default sender */

if (q->sender == NULL) q->sender = copystring(default_sender);


/* If the -t option was found, we have to read the destinations
from inside the message. Destinations read from the command line
are NOT to receive the message. (Not currently noticed here.) */

if (to_inside)
  {
  int done = FALSE;
  int ahead = FALSE; 

  /* Loop for handling an individual header. We have to read ahead
  to check for continuations. */
       
  while (!done && (ahead || read_spool(queue_buffer)))
    {
    char *s, *t; 
    char *p = queue_buffer;
  
    ahead = FALSE;
    if (strncmpic(queue_buffer, "to:", 3) != 0 &&
        strncmpic(queue_buffer, "cc:", 3) != 0 &&
        strncmpic(queue_buffer, "bcc:", 4) != 0) continue;
  
    /* We have an interesting header. Concatenate all its
    continuations if they exist. */
      
    for (;;)
      { 
      p += (int)strlen(p);
      if (read_spool(p+1))
        {
        if (p[1] == ' ' || p[1] == '\t') *p = ' ';
          else { ahead = TRUE; break; }  
        }
      else { done = TRUE; break; }      
      }
      
    /* We now have an unfolded header line in the buffer. */
    
    s = queue_buffer;
    while (*s++ != ':'); 
    while (*s == ' '|| *s == '\t') s++; 
    
    /* Loop for all addresses in the header */
    
    while (*s)
      {
      char *next; 
      char *bra = NULL;   
      t = s;
      
      /* Find the end of this address, and check for '<' while
      we are at it. */
        
      while (*t && *t != ',')
        {
        if (*t == '\"')
          {
          t++; 
          while (*t && *t != '\"') t++;
          if (*t) t++; 
          }  
        else 
          {
          if (*t == '<' && bra == NULL) bra = t; 
          t++;
          } 
        }
        
      /* Set up the next address (before processing the previous,
      because the comma may get overwritten with a null). */
      
      next = t;
      if (*next == ',') next++;
      while (*next == ' ' || *next == '\t') next++;   

      /* Pick out the source address or set up the real address */
      
      if (bra)
        {
        s = bra + 1;
        while (*s && *s != '>') { *s = tolower(*s); s++; }
        if (*s) s[1] = 0;  
        find_dest(q, bra, dest_add);
        }
        
      /* Not a source address - remove anything in brackets, put
      it in angle brackets, and lower case. */
      
      else
        {
        char buff[1024]; 
        char *ss = buff;
        char *tt = s; 
        
        *ss++ = '<';
        while (tt != t)
          {
          if (*tt == '\"')
            {
            *ss++ = *tt++;
            while (tt != t && *tt != '\"') *ss++ = tolower(*tt++);
            if (tt != t) *ss++ = *tt++;
            }   
          else if (*tt == '(')
            {
            while (++tt != t && *tt != ')');
            if (tt != t) tt++;  
            }         
          else 
            {
            int c = tolower(*tt++);  
            if (c != ' ' || ss > buff+1) *ss++ = c;  /* avoids leading sp */
            } 
          } 
        while (ss > buff && ss[-1] == ' ') ss--;     /* avoids trailing sp */   
        strcpy(ss, ">");     
        find_dest(q, buff, dest_add);
        }   
  

      /* Move on to the next address */
      
      s = next;
      }

    /* If we read ahead on another line, shift it down to the
    start of the buffer, and set the flag to cause it to be
    processed rather than reading another line. */  
     
    if (ahead) memmove(queue_buffer, p+1, (int)strlen(p+1) + 1); 
    }
  }

/* Successful exit: we have done what we can and the file does
still exist. */

SUCCESSEXIT:
read_spool_tidy();
return q;

/* Come here for various cases where errors are detected after
getting the store for the block. Tidy it away, and return NULL
for failure. */

ERROREXIT:
read_spool_tidy();
clean_up(q);
return NULL;
}



/*************************************************
*             Find/Create a queue item           *
*************************************************/

/* The queue is kept as a doubly-linked list, sorted by name. However,
to speed up searches, an index into the list is used. This is maintained
by the scan_spool_input function when it goes down the list throwing
out entries that are no longer needed. When the action is "add" and
we don't need to add, mark the found item as seen. */


static debug_queue(void)
{
int i;
int count = 0;
queue_item *p;
printf("\nqueue_total=%d\n", queue_total);

for (i = 0; i < queue_index_size; i++)
  printf("index %d = %d %s\n", i, queue_index[i], (queue_index[i])->name);
  
printf("Queue is:\n");
p = queue_index[0];
while (p != NULL)
  {
  count++;
  for (i = 0; i < queue_index_size; i++)
    { 
    if (queue_index[i] == p) printf("count=%d index=%d\n", count, p); 
    }
  printf("%d %d %d %s\n", p, p->next, p->prev, p->name); 
  p = p->next;
  }  
}   




queue_item *find_queue(char *name, int action)
{
int first = 0;
int last = queue_index_size - 1;
int middle = (first + last)/2;
queue_item *p, *q, *qq;

/* Handle the empty queue as a special case. */

if (queue_total == 0)
  {
  if (action != queue_add) return NULL;
  if ((qq = set_up(name)) != NULL)
    {
    int i;
    for (i = 0; i < queue_index_size; i++) queue_index[i] = qq;
    queue_total++;
    return qq; 
    }
  return NULL;
  } 
  
/* Also handle insertion at the start or end of the queue
as special cases. */

if (strcmp(name, (queue_index[0])->name) < 0)
  {
  if (action != queue_add) return NULL;
  if ((qq = set_up(name)) != NULL)
    {
    qq->next = queue_index[0];
    (queue_index[0])->prev = qq; 
    queue_index[0] = qq; 
    queue_total++;
    return qq; 
    }
  return NULL;
  } 
 
if (strcmp(name, (queue_index[queue_index_size-1])->name) > 0)
  {
  if (action != queue_add) return NULL;
  if ((qq = set_up(name)) != NULL)
    {
    qq->prev = queue_index[queue_index_size-1];
    (queue_index[queue_index_size-1])->next = qq; 
    queue_index[queue_index_size-1] = qq; 
    queue_total++;
    return qq; 
    }
  return NULL;
  } 

/* Use binary chopping on the index to get a range of the queue to search
when the name is somewhere in the middle, if present. */

while (middle > first)
  {
  if (strcmp(name, (queue_index[middle])->name) >= 0) first = middle; 
    else last = middle;
  middle = (first + last)/2;
  }  
  
/* Now search down the part of the queue in which the item must
lie if it exists. Both end points are inclusive - though in fact
the bottom one can only be = if it is the original bottom. */

p = queue_index[first];
q = queue_index[last];

for (;;)
  {
  int c = strcmp(name, p->name);

  /* Already on queue; mark seen if required. */

  if (c == 0)
    {
    if (action == queue_add) p->seen = TRUE;
    return p;
    }

  /* Not on the queue; add an entry if required. Note that set-up might
  fail (the file might vanish under our feet). Note also that we know
  there is always a previous item to p because the end points are 
  inclusive. */

  else if (c < 0)
    {
    if (action == queue_add)
      {  
      if ((qq = set_up(name)) != NULL)
        {
        qq->next = p;
        qq->prev = p->prev;
        p->prev->next = qq;
        p->prev = qq;
        queue_total++;
        return qq; 
        }
      }   
    return NULL;
    }

  /* Control should not reach here if p == q, because the name 
  is supposed to be <= the name of the bottom item. */ 
  
  if (p == q) return NULL;

  /* Else might be further down the queue; continue */

  p = p->next;
  }

/* Control should never reach here. */
} 



/*************************************************
*        Scan the smail spool directory          *
*************************************************/

/* The scan happens only if the directory has been
changed. We count the entries to set the value for
the queue stripchart, and set up data for the
queue display window if the "full" option is given. */

void scan_spool_input(int full)
{
struct stat statdata;

if (stat(input_dir, &statdata)) return;

if (input_mtime != statdata.st_mtime)
  {
  queue_item *p;
  int count = 0; 
  int indexptr = 1; 
  struct dirent *ent;
  DIR *dd = opendir(input_dir);
  if (dd == NULL) return;

  stripchart_total[0] = 0;

  /* Loop for each spool file on the queue - there can be temporary
  files with non-standard names that must also be ignored. Do a
  quick check pro tem. When initializing smailmon, every file will
  have to be read. To show there is progress, output a dot for each
  one to the standard output. If not doing a full queue scan (but
  just counting) don't waste time searching the in-store queue. */

  while ((ent = readdir(dd)) != NULL)
    {
    char *name = ent->d_name;
    if (strcmp(name, ".") != 0 &&
        strcmp(name, "..") != 0 &&
        (int)strlen(name) == 14 &&
        name[6] == '-')
      {
      stripchart_total[0]++;
      if (!smailmon_initialized) { printf("."); fflush(stdout); }
      if (full) find_queue(name, queue_add);
      }
    }
  closedir(dd);
  input_mtime = statdata.st_mtime;
  
  /* If simply counting the number, we are done; likewise if the
  queue is empty. */
   
  if (!full || queue_total == 0) return; 

  /* Now scan the queue and remove any items that were not
  in the directory. At the same time, set up the index pointers
  into the queue. Because we are removing items, the total that
  we are comparing against isn't actually correct, but in a long
  queue it won't make much difference, and in a short queue it
  doesn't matter anyway!*/

  p = queue_index[0];
  while (p != NULL)
    {
    if (!p->seen)
      {
      queue_item *next = p->next;
      if (p->prev == NULL) queue_index[0] = next;
        else p->prev->next = next;
      if (next == NULL) 
        {
        int i; 
        queue_item *q = queue_index[queue_index_size-1];
        for (i = queue_index_size - 1; i >= 0; i--)
          if (queue_index[i] == q) queue_index[i] = p->prev;
        } 
      else next->prev = p->prev;
      clean_up(p);
      queue_total--;
      p = next;
      }
    else
      {
      if (++count > (queue_total * indexptr)/(queue_index_size-1))
        {
        queue_index[indexptr++] = p; 
        }  
      p->seen = FALSE;  /* for next time */
      p = p->next;
      }
    }
    
  /* If a lot of messages have been removed at the bottom, we may not
  have got the index all filled in yet. Make sure all the pointers
  are legal. */
     
  while (indexptr < queue_index_size - 1)
    {
    queue_index[indexptr++] = queue_index[queue_index_size-1]; 
    }  
  }
}




/*************************************************
*         Read the msglog for a message          *
*************************************************/

/* We read it only if its update time differs from last time. */

static void read_msglog(queue_item *p)
{
char buffer[1024];
struct stat statdata;

sprintf(buffer, "%s/../msglog/%s", input_dir, p->name);

if (stat(buffer, &statdata)) return;

if (p->msglog_time != statdata.st_mtime)
  {
  FILE *MSG = fopen(buffer, "r");
  if (MSG == NULL) return; 

  while(fgets(buffer, 1024, MSG) != NULL)
    {
    /* Ensure that failed or succeeded addresses are removed from
    the destination list, and deferred addresses are on the list.
    When removing an address, remove its parent as well, since we
    really just want to show undelivered ultimate addresses. */ 
       
    if (strncmp(buffer, "Xfail:", 6) == 0 || 
        strncmp(buffer, "Xsucceed:", 9) == 0 ||
        strncmp(buffer, "Xdefer:", 7) == 0) 
      {
      char *t; 
      char *s = buffer + 7;
      int action = (buffer[1] == 'd')? dest_add : dest_remove;
        
      while (*s != '<') s++;
      if (s[1] == '<') s++;
      t = s+1;
      while (*t != '>') t++;
      t[1] = 0;
      (void)find_dest(p, s, action);

      /* If appropriate, search for parent, starting after the
      string we just terminated. */
        
      if (action == dest_remove && (s = strstr(t+2, "parent:")) != NULL)
        {
        s += 7;
        while (*s != '<') s++;
        if (s[1] == '<') s++;
        t = s+1;
        while (*t != '>') t++;
        t[1] = 0;
        (void)find_dest(p, s, action);
        }   
      }   
    }   
     
  fclose(MSG);
  p->msglog_time = statdata.st_mtime;
  }
}




/*************************************************
*              Display queue data                *
*************************************************/

/* The present implementation simple re-writes the entire
information each time. Take some care to keep the
scrolled position as it previously was, but, if it was
at the bottom, keep it at the bottom. */

void queue_display(void)
{
int now = (int)time(NULL);
queue_item *p = queue_index[0];

text_empty(queue_widget);

while (p != NULL)
  {
  int count = 1;
  dest_item *dd; 
  char u = 'm';
  int t = (now - p->input_time)/60;  /* minutes on queue */

  if (t > 90)
    {
    u = 'h';
    t = (t + 30)/60;
    if (t > 72)
      {
      u = 'd';
      t = (t + 12)/24;
      }
    }

  read_msglog(p);                   /* update destinations */ 

  /* Can't set this earlier, as msglog data may change things. */

  dd = p->destinations; 

  text_show(queue_widget, "%2d%c %s %s %s\n", t, u, p->name,
    (p->sender == NULL)? "       " : p->sender,
    (dd == NULL)? "" : dd->address); 
    
  if (dd != NULL) dd = dd->next;  
  while (dd != NULL && count++ < queue_max_addresses)
    {
    text_show(queue_widget, "                            %s\n", dd->address);
    dd = dd->next;  
    }  
  if (dd != NULL)
    text_show(queue_widget, "                            ...\n");
  p = p->next;
  }

text_position(queue_widget, 999999);
}

/* End of queue.c */
                      
