/* Expire old messages.
 * Inspired by 'expire.c' by Bernie Roehl.
 * Substantially rewritten for integration into KA9Q NOS,
 * WG7J v1.01 and later
 * by Johan. K. Reinalda, WG7J/PA3DIS, March/April 92
 *
 * Old bid expiry by WG7J, March 92
 * Index file support for jnos 1.10x3 and later added. WG7J Summer 1993
 * Error checking/logging added by N5KNX 14Jun94
 * NNTPS expiry written by VK5XXX, added 28Jun94 and extended for NNTP
 *  for jnos 1.10f -- n5knx
 */

/* 'expire #' sets the expire interval for n hours.
 * Each time the timer goes off, a new process is started,
 * that expires the old messages...
 *
 * The control file '~/spool/expire.dat' contains lists of
 * filename age
 *     -or-
 * !newsgroup age
 *
 * where 'filename' is the name of the .txt file under '~/spool/mail'
 * containing the messages (but specified WITHOUT the ending '.txt').
 * filename can be extended into subdirectories, and can have either
 * '/', '\' or '.' to indicate subdirectories.
 *
 * and where 'newsgroup' is the NNTP newsgroup name whose articles are
 * to be expired.  The active and history files are also updated when
 * necessary.
 *
 * 'age' is an integer giving the maximum age of a message or newsgroup
 * article, in days, after which expiry is performed.
 * If no age is given, the default age is 21 days.
 */
#ifdef MSDOS
#include <dir.h>
#include <dos.h>
#endif
#include <alloc.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/timeb.h>
#include <ctype.h>
#include <time.h>
#include "global.h"
#include "timer.h"
#include "proc.h"
#include "bm.h"
#include "files.h"
#include "smtp.h"
#include "socket.h"
#include "index.h"
#ifdef NNTPS
#include "dirutil.h"
#include <limits.h>
#endif

#if defined(NNTPS) || defined(NNTP)
#define NN_EITHER
#endif

#ifdef LINUX
extern int unlink __ARGS((char *));
#endif
  
#ifndef __BORLANDC__
#ifndef UNIX
time_t mktime(struct tm *);
  
/* If you're using BC++ 2.0 or higher, you don't need this,
 * but TCC 2.0 doesn't have it... (But, TC++ v1.01 does)
 */
/* Simple emulation of the mktime() call (sort-of works :-) )
 * doesn't do any error checking,
 * no timezone adjustments or value adjustments
 * Simply 'sort-a' calculates the number of seconds since 1970 - WG7J
 */
time_t
mktime(t)
struct tm *t;
{
    static int total[12]={0,31,59,90,120,151,181,212,243,273,304,334};
    int years;
    int leapyears;
    int days;
  
    /* time count start at jan 1, 1970, 00:00 hrs */
    years = t->tm_year - 70;
    /* adjust for leap-years */
    leapyears = (years + 2) / 4;
    if (!((years+70) & 3) && (t->tm_mon < 2))
        --leapyears;    /* no extra day until 3/1 or after */
  
    days = years*365L + leapyears + total[t->tm_mon] + (t->tm_mday-1);
  
    return( days*86400L + t->tm_hour*3600L + t->tm_min*60L + t->tm_sec);
}
#endif  /* UNIX */
#endif  /* __BORLANDC__ */
  
/* Include the rest in #ifdef's, so we don't pull in the whole module
 * when we only have AT defined and NOT EXPIRE !
 * (since these are the two modules that needs the surrogate mktime()
 *  if compiling with Turbo C 2.0 )
 */
#ifdef EXPIRY
  
/* Default expiry values: */
#define DEFAULT_AGE 21       /* 21 days from origination date */
#define MSPHOUR (1000L*60L*60L)
#define DAYStoSECS (24L*60L*60L)
static struct timer Expiretimer;
static int Eproc;
  
static char OpenErr[] = "expire: err %d opening %s";
static char RenErr[] = "expire: err %d renaming %s to %s";
static char WriteErr[] = "expire: err %d writing %s";
static char IRMsg[] = "expire: err %d reading index for msg %d of %s";
static char IWMsg[] = "expire: err %d writing index for msg %d of %s";
static char TRMsg[] = "expire: err %d reading msg %d of %s";
static char TWMsg[] = "expire: err %d writing msg %d to %s";

static void Expireprocess __ARGS((int a,void *v1,void *v2));
static void Expiretick __ARGS((void *));
static void expire __ARGS((char *,int));
static void Oldbidtick __ARGS((void *p));
/* NNTP Expire stuff */
#ifdef NN_EITHER
static void update_nntp_history __ARGS((int));
static int convert_num __ARGS((char **));
#endif
#ifdef NNTPS
static void expire_nntpsrv __ARGS((char *, int));
static void update_nntp_active __ARGS((void));
static void update_newsrc __ARGS((int, int, char *));
static char *newsgroup_to_path __ARGS((char *));
#endif
  
int
doexpire(argc,argv,p)
int argc;
char *argv[];
void *p;
{
    if(argc < 2) {
        tprintf("timer: %lu/%lu hrs\n",
        read_timer(&Expiretimer)/MSPHOUR,
        dur_timer(&Expiretimer)/MSPHOUR);
        return 0;
    }
    if(*argv[1] == 'n') {
        Expiretick(NULL);
        return 0;
    }
    /* set the timer */
    stop_timer(&Expiretimer); /* Just in case */
    Expiretimer.func = (void (*)())Expiretick;/* what to call on timeout */
    Expiretimer.arg = NULL;        /* dummy value */
    set_timer(&Expiretimer,atol(argv[1])*MSPHOUR); /* set timer duration */
    start_timer(&Expiretimer);
    return 0;
}
  
void
Expiretick(p)
void *p;
{
    start_timer(&Expiretimer);
    /* Spawn off the process */
    if(!Eproc)
        newproc("Expiry", 1536, Expireprocess, 0, NULL, NULL, 0);
}
  
static void
Expireprocess(a,v1,v2)
int a;
void *v1, *v2;
{
    char line[80];
    int age;
#ifdef NN_EITHER
    int expire_nntp_history = 0;
#endif
    char *cp;
    FILE *ctl;
  
    Eproc = 1;
    if ((ctl = fopen(Expirefile, "r")) == NULLFILE) {
        Eproc = 0;
        return;
    }
    /* read lines from the control file */
    while(fgets(line, sizeof(line), ctl) != NULLCHAR) {
        pwait(NULL); /* be nice */
        if((*line == '#') || (*line == '\n')
#ifndef NN_EITHER
           || (*line == '!')
#endif
          ) /* comment or blank line */
            continue;
        rip(line);
        /* terminate area name */
        age = DEFAULT_AGE;
        if((cp=strpbrk(line, " \t")) != NULLCHAR) {
            /* there is age info */
            *cp++ = '\0';
            age = atoi(cp);
            if (age <= 0) age = DEFAULT_AGE;
        }
#ifdef NN_EITHER
        if (*line == '!') {  /* Expire NNTP entry if line begins with ! */
#ifdef NNTPS
            expire_nntpsrv(&line[1], age);
#endif
#ifdef NNTP
            expire(&line[1], age);
#endif
            if (expire_nntp_history < age)
                expire_nntp_history = age;
        }
        else
#endif
        expire(line,age);
    }
    fclose(ctl);
#ifdef NN_EITHER
    if (expire_nntp_history) {
        update_nntp_history(expire_nntp_history);
#ifdef NNTPS
        update_nntp_active();  /* and news.rc */
#endif
    }
#endif
    Eproc = 0;
}
  
#ifdef NN_EITHER
/*
        expire NNTP articles
        coded by brett england (future amateur) and rob vk5xxx.
*/
static
int
convert_num( p )
char **p;
{
    int i;
    char *pp = *p;

    i = (*pp - '0') * 10 + *(pp+1) - '0';
    (*p)+=2;
    return (i);
}

static
void
update_nntp_history(age)
int age;
{
  FILE *old, *new;
  char newfile[FILE_PATH_SIZE];
  char line[LINELEN];
  char *p;
  struct tm tm_time;
  time_t expire_time, file_time, now;
  int i,history_records_expired = 0;

  for (i=10; i>0; i--) {
      if(!mlock(History,NULLCHAR)) break;
      pause(2000L);
  }
  if (!i)  return;   /* can't lock, we'll have to wait until next time */


  expire_time = (long)age * DAYStoSECS;  /* days to seconds */
  time(&now);

  sprintf(newfile,"%s.new",History);
  unlink(newfile);
  if((old = fopen(History,READ_TEXT)) == NULLFILE) {
      log(-1,OpenErr, errno, History);
      rmlock(History,NULLCHAR);
      return;
  }

  if((new = fopen(newfile,WRITE_TEXT)) == NULLFILE) {
      fclose(old);
      log(-1,OpenErr, errno, newfile);
      rmlock(History,NULLCHAR);
      return;
  }

  /* Scan the history file, deleting old article references */
  /* format: <message-id> yymmdd hhmmss ngname/Art# */
  for(;;) {
    pwait(NULL);
    if(fgets(line, LINELEN, old) == NULL)
        break;

    p = line;
    while (*p != '\0' && *p != '>')
       p++;
    if (*p != '>')  /* Some sort of corrupt line */
        continue;
    p+=2;
    tm_time.tm_year = convert_num(&p);
    tm_time.tm_mon = convert_num(&p)-1;
    tm_time.tm_mday = convert_num(&p);
    p++;   /* Point to time component */
    tm_time.tm_hour = convert_num(&p);
    tm_time.tm_min = convert_num(&p);
    tm_time.tm_sec = 0;

    file_time = mktime(&tm_time);

    if (file_time > ( now - expire_time ))
      fputs(line, new);
    else
      history_records_expired++;
  }
  i=ferror(new);
  fclose(new);
  fclose(old);

  if(!i) {
      unlink(History);
      if(rename(newfile, History) == -1)
          log(-1, RenErr, errno, newfile, History);
  }
  else log(-1, WriteErr, errno, newfile);

  rmlock(History,NULLCHAR);

  if(history_records_expired > 0)
    log(-1,"Expired: %d in %s", history_records_expired, History);
}
#endif

#ifdef NNTPS
/* Check the news.rc file if the number contained is less than min
   replace the news.rc file contents with min.
*/
static
void
update_newsrc( min, max, path )
  int min, max;
  char *path;
{
  FILE *newsrcfd;
  char news_file[FILE_PATH_SIZE], line[LINELEN];
  int no;

  sprintf(news_file,"%s/news.rc",path);
  if((newsrcfd = fopen(news_file,"r+")) == NULL) {
      log(-1,OpenErr, errno, news_file);
      return;
  }

  fgets(line, LINELEN, newsrcfd);
  rewind(newsrcfd);

  no = atoi(line);
  if((no < min) || (no > max))
      fprintf(newsrcfd,"%d",min);
  fclose(newsrcfd);

}

/* Update the active file and return the lowest news article
*/
static
void
update_nntp_active() {
  FILE *old, *new;
  char newfile[FILE_PATH_SIZE];
  char line[LINELEN];
  char *p, *path, *rpath;
  int min, max;
  int command, file_num;
  struct ffblk *file;


  sprintf(newfile,"%s.new",Active);
  unlink(newfile);
  if((old = fopen(Active,READ_TEXT)) == NULLFILE) {
      log(-1,OpenErr,errno,Active);
      return;
  }

  if((new = fopen(newfile,WRITE_TEXT)) == NULLFILE) {
      fclose(old);
      log(-1,OpenErr,newfile);
      return;
  }

  file = (struct ffblk *)mallocw(sizeof(struct ffblk));
  /* rewrite the active file, with current max and min article numbers */
  /* format: ngname MaxArt# MinArt# Postflag */
  for(;;) {
    pwait(NULL);
    if(fgets(line, LINELEN, old) == NULL)
       break;

    p = line;
    while (*p != '\0' && ! isspace(*p))
      p++;
    if (*p == '\0')
      continue;
    *p = '\0';

    p++;
    while(*p != '\0' && (isspace(*p) || isdigit(*p)))
        p++;

    rpath = newsgroup_to_path( line );
    path = wildcardize(rpath);

    command = 0;
    min = INT_MAX;
    max = INT_MIN;
    for(;;) {
        pwait(NULL);
        if (!nextname(command, path, file))
            break;
        command = 1;   /* Find next */

        if (file->ff_name[0] == '.')
            continue;  /* ignore . and .. */

        file_num = atoi(file->ff_name);
        if (file_num > 0) {     /* should always be greater than 0 */
            if (file_num < min)
                min = file_num;
            if (file_num > max)
                max = file_num;
        } 
    }
    if (min == INT_MAX && max == INT_MIN) { /* No files */
        min = 1;
        max = 0;
    }
    update_newsrc(min, max, rpath);

    fprintf(new,"%s %05d %05d %s", line, max, min, p);
  }

  command=ferror(new);
  fclose(new);
  fclose(old);
  free(file);

  if(!command) {
      unlink(Active);
      if(rename(newfile,Active) == -1)
          log(-1,RenErr,newfile,Active);
  }
  else log(-1, WriteErr, errno, newfile);
}

static
char
*newsgroup_to_path( group )
char *group;
{
    FILE *f;
    static char line[LINELEN];
    char *cp;

    if((f = fopen(Pointer,"r")) == NULL)
        return((char *)NULL);

    for (;;) {
        pwait(NULL);
        if (fgets(line,LINELEN,f) == NULL)
            break;

        if (strcspn(line," ") != strlen(group))
            continue;

        if (strnicmp(group,line,strlen(group)) == 0) {
            cp = (strchr(line,' ')) + 1;

            rip(cp);
            fclose(f);
            return (cp);
        }
    }
    fclose(f);
    return (NULL);
}

void
expire_nntpsrv(nntp_name, age)
char *nntp_name;
int age;
{
    char *path;
    int command = 0;
    struct ffblk file;
    struct tm tm_time;
    time_t file_time, expire_time, now;
    char nntp_file[FILE_PATH_SIZE];
    char save_path[FILE_PATH_SIZE];
    int file_num, expired = 0;

    /* Resolve nntp name into directory path */
    if ((path = newsgroup_to_path( nntp_name )) == NULL)
        return;

    expire_time = (long)age * DAYStoSECS;
    time(&now);
    strcpy(save_path, path);
    path = wildcardize(path);

    for(;;) {
        pwait(NULL);
        if (!nextname(command, path, &file))
            break;
        command = 1;   /* Find next */

        if (file.ff_name[0] == '.')
            continue;  /* ignore . and .. */

        file_num = atoi(file.ff_name);
        if (file_num > 0) {
            /* only expire a file that is numeric :-) */
            tm_time.tm_sec = 0;  /* DOS doesn't store this */
            tm_time.tm_min = (file.ff_ftime >> 5) & 0x3f;
            tm_time.tm_hour = (file.ff_ftime >> 11) & 0x1f;
            tm_time.tm_mday = file.ff_fdate & 0x1f;
            tm_time.tm_mon = ((file.ff_fdate >> 5) & 0xf)-1;
            tm_time.tm_year = (file.ff_fdate >> 9) + 80;

            file_time = mktime(&tm_time);

            if (( now - expire_time ) > file_time) {
                 sprintf(nntp_file, "%s/%s", save_path, file.ff_name);
                 unlink(nntp_file);
                 expired++;
            }
        }
    }
    if(expired)
        log(-1,"Expired: %d in %s",expired, nntp_name);
}
#endif NNTPS

void
expire(filename,age)
char *filename;
int age;
{
    int idx,idxnew,expired,i,err;
    FILE *txt,*new;
    long start,pos,msgsize;
    time_t now;
    struct indexhdr hdr;
    struct mailindex index;
    char file[FILE_PATH_SIZE];
    char newfile[FILE_PATH_SIZE];
    char buf[LINELEN];

#define IRERR	-1	/* Index read error */
#define IWERR	-2	/* Index write error */
#define TRERR	-3	/* area text file read error */
#define TWERR	-4	/* area text file write error */
#define TPERR	-5	/* area text file position error */

    dirformat(filename);
  
    if(mlock(Mailspool,filename))
        /* can't get a lock */
        return;
  
    sprintf(file,"%s/%s.txt",Mailspool,filename);
    if((txt=fopen(file,READ_BINARY)) == NULLFILE) {
        rmlock(Mailspool, filename);
        return;
    }

    sprintf(file,"%s/%s.ind",Mailspool,filename);
    if((idx=open(file,READBINARY)) == -1) {    /* Open the index file */
        fclose(txt);
        log(-1, OpenErr, errno, file);
        rmlock(Mailspool,filename);
        return;
    }

    /* Create new text file */
    sprintf(file,"%s/%s.new",Mailspool,filename);
    if((new=fopen(file,WRITE_TEXT)) == NULLFILE) {
        fclose(txt);
        close(idx);
        log(-1, OpenErr, errno, file);
        rmlock(Mailspool, filename);
        return;
    }
  
    /* Create the new index file */
    sprintf(file,"%s/%s.idn",Mailspool,filename);
    if((idxnew=open(file,CREATETRUNCATEBINARY,CREATEMODE)) == -1) {
        fclose(txt);
        close(idx);
        fclose(new);
        rmlock(Mailspool,filename);
        log(-1, OpenErr, errno, file);
        return;
    }
  
    memset(&index,0,sizeof(index));
    time(&now);
    start = pos = 0L;
    expired = err = 0;
  
    /* Write a default header to the new index file */
    default_header(&hdr);
    if (write_header(idxnew,&hdr) == -1) {
        log(-1, IWMsg, errno, -1, filename);
        err=IWERR;
    }
  
    /* Read the header from the index file */
    if (read_header(idx,&hdr) == -1) {
        log(-1, IRMsg, errno, -1, filename);
        err=IRERR;
    }
  
    /* Now read all messages, and expire old ones */
    for(i = 1; i <= hdr.msgs && !err; i++) {
        default_index(filename,&index);
        if (read_index(idx,&index) == -1) {
            log(-1,IRMsg,errno,i,filename);
            err=IRERR;
            break;
        }
        msgsize = index.size;
        if(now - index.date < ((long)age*DAYStoSECS)) {
            /* This one we should keep ! Copy it from txt to new */
            fseek(txt,start,SEEK_SET);
            pos = ftell(new);
            /* Read the first line, should be 'From ' line */
            if (bgets(buf,sizeof(buf),txt) == NULL) {
                log(-1, TRMsg, errno,i,filename);
                err=TRERR;
                break;
            }
	    if (strncmp(buf,"From ",5)) {
                log(-1, "expire: format error in msg %d of %s.txt",i,filename);
                err=TPERR;
                break;
            }

            /* Now copy to output until we find another 'From ' line or
             * reach end of file.
             */
            do {
                if (fprintf(new,"%s\n",buf) == -1) {
                    log(-1, TWMsg,errno,i,filename);
                    err=TWERR;
                    break;  /* copy of msg will have shrunk ...  */
                }
                if (bgets(buf,sizeof(buf),txt) == NULL) break;
                pwait(NULL);
            } while (strncmp(buf,"From ",5));
            /* write the index for the new copy of the message */
            index.size = ftell(new) - pos;
            if (index.size < 0 || pos < 0) {
                log(-1, "expire: ftell err %d for %s", errno, filename);
                err=TPERR;
                break;
            }
            if (WriteIndex(idxnew,&index) == -1) {
                log(-1,IWMsg,errno,i,filename);
                err=IWERR;
                break;
            }
        } else
            expired++;
        start += msgsize;    /* starting offset of the next message */
    }
    default_index("",&index);
    close(idx);
    close(idxnew);
    pos=ftell(new);        /* how long is new area */
    fclose(new);
    fclose(txt);

    if (!err) {

        sprintf(file,"%s/%s.txt",Mailspool,filename);
        sprintf(newfile,"%s/%s.new",Mailspool,filename);
        unlink(file);
        if(pos)
            rename(newfile,file);
        else
            /* remove a zero length file */
            unlink(newfile);
        sprintf(file,"%s/%s.ind",Mailspool,filename);
        sprintf(newfile,"%s/%s.idn",Mailspool,filename);
        unlink(file);
        if(pos)
            rename(newfile,file);
        else {
            /* remove a zero length file */
            unlink(newfile);
#ifdef USERLOG
            sprintf(file,"%s/%s.inf", Mailspool, filename);
            unlink(file);
#endif
        }
    }
    rmlock(Mailspool,filename);
    if(expired)
        log(-1,"Expired: %d in %s",expired,filename);

}
  
  
/******************************************************************/
/* This program will deleted old BID's from the history file,
 * after making a backup copy.
 *
 *  Eg. 'oldbids 24 30' will try to delete all bids older then 30 days
 *      every 24 hours.
 *      'oldbids now' will do it now, with set value of age.
 *
 * Copyright 1992, Johan. K. Reinalda, WG7J/PA3DIS
 *      email : johan@ece.orst.edu
 *      packet: wg7j@wg7j.or.usa.na
 *
 * Any part of this source may be freely distributed for none-commercial,
 * amateur radio use only, as long as credit is given to the author.
 *
 * v1.0 920325
 */
static struct timer Oldbidtimer;
static int Oldbid_age = 30;
  
static
void
Oldbidtick(p)
void *p;
{
    int i,expired = 0;
    char *cp;
    FILE *old, *new;
    time_t now;
    time_t age;
    time_t bidtime;
    #define LEN 80
    char newfile[LEN];
    char buf[LEN];
  
    stop_timer(&Oldbidtimer);
  
    /* We will rewrite the msgid history file, renaming it when we are
       done.  Since this is not protected by a lock, we won't call pwait()
       so as to minimize chances we'll lose an update -- N5KNX */

    sprintf(newfile,"%s.new",Historyfile);
    unlink(newfile);
    if((old = fopen(Historyfile,READ_TEXT)) == NULLFILE) {
        log(-1, OpenErr, errno, Historyfile);
        return;
    }
    if((new = fopen(newfile,WRITE_TEXT)) == NULLFILE) {
        fclose(old);
        log(-1, OpenErr, errno, newfile);
        return;
    }

    now = time(&now);
    age = (time_t)(Oldbid_age*DAYStoSECS);
  
    while(fgets(buf,LEN,old) != NULL) {
        rip(buf);
        if((cp=strchr(buf,' ')) != NULLCHAR) {
            /*found one with timestamp*/
            *cp = '\0';
            cp++;   /* now points to timestamp */
            if((bidtime = atol(cp)) == 0L)
                /*something wrong, re-stamp */
                fprintf(new,"%s %ld\n",buf,now);
            else {
                /* Has this one expired yet ? */
                if(now - bidtime < age)
                    fprintf(new,"%s %ld\n",buf,bidtime);
                else
                    expired++;
            }
        } else {
            /* This is an old one without time stamp,
             * add to the new file with current time as timestamp
             */
            fprintf(new,"%s %ld\n",buf,now);
        }
    }

    i=ferror(new);
    fclose(old);
    fclose(new);
  
    if(!i) {
        unlink(Historyfile);
        if(rename(newfile,Historyfile) == -1)
            log(-1, RenErr, errno, newfile, Historyfile);
    }
    else log(-1, WriteErr, errno, newfile);

    if(expired)
        log(-1,"Oldbids: %d expired",expired);

    start_timer(&Oldbidtimer);
    return;
}
  
int
dooldbids(argc,argv,p)
int argc;
char *argv[];
void *p;
{
    if(argc < 2){
        tprintf("timer: %lu/%lu hrs; age: %d days\n",
        read_timer(&Oldbidtimer)/MSPHOUR,
        dur_timer(&Oldbidtimer)/MSPHOUR,
        Oldbid_age);
        return 0;
    }
    if(*argv[1] == 'n') {
        Oldbidtick(NULL);
        return 0;
    }
    /* set the timer */
    stop_timer(&Oldbidtimer); /* Just in case */
    Oldbidtimer.func = (void (*)())Oldbidtick;/* what to call on timeout */
    Oldbidtimer.arg = NULL;        /* dummy value */
    set_timer(&Oldbidtimer,atol(argv[1])*MSPHOUR); /* set timer duration */
    if(argc > 2)
        Oldbid_age = atoi(argv[2]);
    Oldbidtick(NULL); /* Do one now and start it all!*/
    return 0;
}
  
#endif /* EXPIRE */
