/*
 * W-NEWS       A simple NEWS processing package for MINIX.
 *              This program takes care of removing (and/or archiving)
 *              articles which have expired.  It also updates the counts
 *              in the "active" file.
 *
 * Usage:       expire [-d] [-n groups] -a [groups] [-e days] [-v level]
 *                     [-f user] [-p] [-t archiver]
 *
 * Notes:       -d = -v3 and "do not act"
 *              -t "archiver %f %a %s ..."
 *              as in '-t "lharc a %f %a [ -s %s ]"'
 *
 * Version:     @(#)expire.c	4.50	03-Feb-1995
 *
 * Authors:     Miquel van Smoorenburg, <miquels@drinkel.ow.org>
 *		Udo Munk, <udo@umunk.GUN.de> (Coherent)
 *
 *		Copyright 1988-1993 MicroWalt Corporation
 *		Copyright 1994,1995 Cistron Software C.V.
 *		This program is free software; you can redistribute it and/or
 *		modify it under the terms of the GNU General Public License
 *		as published by the Free Software Foundation; either version
 *		2 of the License, or (at your option) any later version.
 */
#include "wnews.h"


#if HAVE_DBM
#  if HAVE_SDBM
#   include <sdbm.h>
#  else
#    include <ndbm.h>
#  endif
#endif

#ifndef BLOCK_SIZE
#   define BLOCK_SIZE	1024
#endif


typedef struct article {
  int		stat_days;
  int		art_days;
  int		seq;
  char		from[128];
  char		subject[128];
  struct article *next;
} ART;


char *Release = VERSION;
char *Version = "@(#) expire 4.50 (03-Feb-1995)";


int e_debug = 0;			/* debug flag.			*/
int errflg = 0;                         /* an error has occurred        */
int expiretime = 15;                    /* expiration interval          */
int usepostdate = 0;                    /* use Date: field in article   */
int verbose = 1;                        /* be verbose about it all      */
int needsubject = 0;                    /* we need a Subject: def.      */
char *archv[16];                        /* archiver definition          */
int archc = 0;                          /* will we use an archiver?     */
char *usefrom = (char *)NULL;           /* look at the From: line ?     */
char *grouplist = (char *)NULL;		/* List of groups to do		*/
char *archlist = (char *)NULL;		/* Archive-list.		*/
ACT *active;				/* The active file.		*/
char *program;				/* For logmsg().		*/


extern char *months[];			/* In logmsg.c */


/* For getopt(3) */
extern int getopt(), optind;
extern char *optarg;
#ifndef COHERENT
extern int opterr;
#endif


/* Prototypes of functions in this file. */
_PRO( char *nextstring, (char **linep, int last, char *message)		);
_PRO( void opt_t, (char *)						);
_PRO( int match, (char *s, char **t)					);
_PRO( int parsedate, (char *s, time_t t)				);
_PRO( void examine, (ACT *a, ART *art, char *file,			\
		     struct stat *st, time_t tim)			);
_PRO( char *store, (ART *art, ACT *a)					);
_PRO( void walk, (void) 						);
_PRO( void trap, (int sig)						);

#if HAVE_HIST

#if HAVE_DBM
/* Copy an entry to mallloced memory. */
datum copy(orig)
datum orig;
{
  datum ret;

  if (orig.dptr && (ret.dptr = malloc(orig.dsize)) != NULL) {
	memcpy(ret.dptr, orig.dptr, orig.dsize);
	ret.dsize = orig.dsize;
  } else {
	ret.dptr = NULL;
	ret.dsize = 0;
  }
  return(ret);
}
#endif

/* Walk through the history file(s) */
void do_history()
{
  char fname[128];
  char hname[128];
  FILE *fp;
  char *bp;
#if HAVE_DBM
  char *sp;
  DBM *db;
  datum key, data, next;
  int len, size;

  if (verbose) logmsg(LOGINFO, "expire: examining history file(s)\n");

  /* First, go to the spool directory. */
  if (chdir(SPOOLDIR) < 0) {
	logmsg(LOGERR|LOGSYS, "cannot chdir(%s)\n", SPOOLDIR);
	exit(1);
  }

  /* Open dbase file read/write and temp ascii file. */
  sprintf(hname, "%s/%s", VARDIR, HISTFILE);
  if ((db = dbm_open(hname, O_RDWR, 0)) == NULL) {
	logmsg(LOGSYS|LOGERR, "cannot open dbase file %s\n", hname);
	return;
  }
  strcpy(fname, hname);
  strcat(fname, ".tmp");
  if ((fp = fopen(fname, "w")) == NULL) {
	logmsg(LOGSYS|LOGERR, "cannot open %s\n", fname);
	dbm_close(db);
	return;
  }

  /* Read dbase file one by one and decide which articles are obsolete. */
  /* XXX - this barfs - MvS */
  key = dbm_firstkey(db);
  key = copy(key);
  next.dptr = NULL;
  while (key.dptr != NULL) {
	if (next.dptr) free(next.dptr);
	next = copy(dbm_nextkey(db));
#if 0
	/* Gelul, natuurlijk! */
	if (next.dptr == NULL) {
		logmsg(LOGERR, "history dbm file: cannot dbm_nextkey\n");
	}
#endif

	data = dbm_fetch(db, key);
	if (data.dptr == NULL) {
		logmsg(LOGERR, "dbm file bad: %s not found\n", key.dptr);
		if (key.dptr) free(key.dptr);
		key = copy(next);
		continue;
	}
	sp = (char *)data.dptr + 12; /* Skip time stamp. */
	size = data.dsize - 12;

	if (verbose > 2) logmsg(LOGINFO, "expire: examining msgid %s\n", key.dptr);

	/* Loop over all articles. */
	while(size > 0) {
		sscanf(sp, "%d", &len);
		/* Build filename. */
		strcpy(fname, sp + 4);
		for(bp = fname; *bp; bp++) if (*bp == '.') *bp = '/';
		/* See if it's there. */
		if (verbose > 2) logmsg(LOGINFO, "expire: testing %s\n", fname);
		if (access(fname, F_OK) != 0) {
			/* Delete this entry. */
			data.dsize -= len;
			memcpy(sp, sp + len, size - len);
		} else {
			/* Don't touch this entry. */
			sp += len;
		}
		size -= len;
	}
	/* Any articles left? */
	if (data.dsize > 12) {
		/* Write out ASCII entries. */
		hist_print(key.dptr, data.dptr, data.dsize, fp);
		if (key.dptr) free(key.dptr);
		key = copy(next);
	} else {
		/* Delete this DBM entry totally. */
		dbm_delete(db, key);
		if (key.dptr) free(key.dptr);
		key = copy(next); /*copy(dbm_fetch(db, next));*/
	}
  }
  dbm_close(db);
#else /* !HAVE_DBM */
  FILE *infp;
  char buf[128];
  char *msgid, *date, *art;

  if (verbose) logmsg(LOGINFO, "examining history file\n");

  /* First, go to the spool directory. */
  if (chdir(SPOOLDIR) < 0) {
	logmsg(LOGERR|LOGSYS, "cannot chdir(%s)\n", SPOOLDIR);
	exit(1);
  }

  /* Open history file and temp ascii file. */
  sprintf(hname, "%s/%s", VARDIR, HISTFILE);
  if ((infp = fopen(hname, "r")) == NULL) {
	logmsg(LOGSYS|LOGERR, "cannot open(%s)\n", fname);
	return;
  }
  strcpy(fname, hname);
  strcat(fname, ".tmp");
  if ((fp = fopen(fname, "w")) == NULL) {
	logmsg(LOGSYS|LOGERR, "cannot open(%s)\n", fname);
	fclose(infp);
	return;
  }

  /* Walk over history file to see which articles have been deleted. */
  while(fgets(buf, 128, fp)) {
	msgid = strtok(buf, "\t");
	date  = strtok(NULL, "\t");
	art   = strtok(NULL, "\n");
	if (msgid == NULL || date == NULL || art == NULL) continue;

	/* Create filename and see if the file is there. */
	strcpy(fname, art);
	for(bp = fname; *bp; bp++) if (*bp == '.') *bp = '/';

	if (access(fname, F_OK) == 0)
		fprintf(fp, "%s\t%s\t%s\n", msgid, date, art);
  }
  fclose(infp);
#endif /* HAVE_DBM */

  /* Close temp histfile and rename. */
  fclose(fp);
  sprintf(fname, "%s.tmp", hname);
  if (unlink(hname) < 0) {
	logmsg(LOGSYS|LOGERR, "cannot unlink(%s)\n", hname);
	return;
  }
  if (rename(fname, hname) < 0) {
	logmsg(LOGSYS|LOGERR, "cannot rename(%s,%s)\n", fname, hname);
	return;
  }

  /* Done! */
}

#endif /* HAVE_HIST */

/* Return next string in a line. */
char *
nextstring(linep, last, message)
char **linep;
int last;
char *message;
{
  char *s;
  
  s = *linep;
  if (*s == 0) return(s);

  while(**linep && **linep != ' ' && **linep != '\t' && **linep != '\n')
								(*linep)++;
  if (**linep == '\n') **linep = '\0';  
  if (**linep == '\0' && !last) {
	if (message) logmsg(LOGERR, message);
	return("");
  }
  if (**linep == 0) return(s);

  *(*linep)++ = '\0';
  for (; **linep == ' ' || **linep == '\t'; (*linep)++)
		    ;
  for (; *s == ' ' || *s == '\t'; s++)
		    ;
  return(s);
}


/* See if string is in table. */
int
match(s, t)
char *s;
char **t;
{
  int f;
  for(f = 0; t[f]; f++)
	if (strncmp(t[f], s, 3) == 0) return(f);
  return(0);
}


/*
 * Turn date string into time.
 * There are 3 formats: 8 Dec 90 00:00... 
 *                      Sat, 8 Dec 90 00:00..
 *                      Sat Jan 12 00:00:00 1991
 * Include thorough checking to be sure no wrong date is calculated!
 */
int
parsedate(s, t)
char *s;
time_t t;
{
  char *p;
  struct tm tm;
  time_t tim;
  int day, year, mon;
  
  if (*(p = nextstring(&s, 1, (char *)NULL)) == '\0') return(-1);

  if (isdigit(*p)) {
	day = atoi(p);
	mon = match(nextstring(&s, 0, (char *)NULL), months);
	year = atoi(nextstring(&s, 0, (char *)NULL));
  } else if (isdigit(*(p = nextstring(&s, 0, (char *)NULL)))) {
	day = atoi(p);
	mon = match(nextstring(&s, 0, (char *)NULL), months);
	year = atoi(nextstring(&s, 0, (char *)NULL));
  } else {
	mon = match(p, months);
	day = atoi(nextstring(&s, 0, (char *)NULL));
	nextstring(&s, 0, (char *)NULL);
	year = atoi(nextstring(&s, 0, (char *)NULL));
  }
  if (day == 0 || mon == 0 || year == 0) return(-1);

  if (year > 1900) year -= 1900;        
  tm.tm_sec = 0;
  tm.tm_min = 0;
  tm.tm_hour = 0;
  tm.tm_mday = day;
  tm.tm_mon = mon - 1;
  tm.tm_year = year;
  tim = wmktime(&tm); 
  if (tim < 0) return(-1);
  if (tim > t) tim = t;
  return((t - tim) / 86400L);
}


/* Make a list of arguments for the -t option. */
void
opt_t(ar)
char *ar;
{
  int fileseen = 0;
  int artseen = 0;
  char *s;
  int f;
  
  for(f = 0; f < 15; f++) {
	s = nextstring(&ar, 1, NULL);
	if (*s == 0) break;
	if (strcmp(s, "%f") == 0) fileseen++;
	if (strcmp(s, "%a") == 0) artseen++;
	if (strcmp(s, "%s") == 0) needsubject = 1;      
	archv[f] = s;
  }
  archc = f;
  archv[f] = (char *)NULL;

  if (f == 0 || fileseen == 0 || artseen == 0) {
	errflg++;
	logmsg(LOGERR, "-t: wrong format\n");
  } else if (e_debug) {
	logmsg(LOGINFO, "expire: archiver: %s\n", archv[f]);
  }
}
 

/* Examine article, fill in structure and maybe open it to read data. */
void
examine(a, art, file, st, tim)
ACT *a;
ART *art;
char *file;
struct stat *st;
time_t tim;
{
  char line[256];
  char dat[128];
  char *date = (char *)NULL;
  register char *s;
  register char *t;
  FILE *fp;
  int li = 0;
  
  art->stat_days = (tim - st->st_mtime) / 86400L;
  art->art_days = art->stat_days;
  strcpy(art->subject, "(not read)");
  strcpy(art->from, "(not read)");
  art->seq = atol(file);
  
  if (!needsubject && usefrom == (char *)NULL && usepostdate == 0) return;

  if ((fp = fopen(file, "r")) == (FILE *)NULL) {
	logmsg(LOGERR, "cannot open(%s) in newsgroup %s", file, a->group);
	return;
  }
  
  while(li++ < 20 && fgets(line, 256, fp) != (char *)NULL) {
	if (*line == '\n') break;
	line[127] = '\0';
	if (strncmp(line, "From:", 5) == 0) {
		t = line + 6;
		s = art->from;
		while (*t && *t != ' ' && *t != '\t' && *t != '\n')
							*s++ = *t++;
		*s = '\0';
	}
	if (strncmp(line, "Subject:", 8) == 0) {
		strcpy(art->subject, line + 9);
		for (s = art->subject; *s; s++)
			if (*s == '\n') *s = '\0';
	}
	if (strncmp(line, "Date:", 5) == 0) {
		date = dat;
		strcpy(dat, line + 6);
	}
  }
  (void) fclose(fp);
  if (date) {
	strcpy(line, date);
	if ((art->art_days = parsedate(date, tim)) < 0) {
		logmsg(LOGERR, "article %s: cannot parse date %s",
							file, line);
		art->art_days = art->stat_days;
	}
  } else art->art_days = art->stat_days;
}


/* Store (archive) an article. */
char *
store(art, a)
ART *art;
ACT *a;
{
  struct stat st;
  int f, g;
  static char buf1[256];
  char buf2[256];
  char seq[16];
  char *args[16];

  char *s;
  int status, pid;
  
  if (stat(OLDNEWS, &st) < 0) {
	logmsg(LOGERR, "can't archive: no directory %s\n", OLDNEWS);
	return((char *)NULL);
  }

  /* Now build path and make directories as we go. */
  for(f = 0; ; f++) {
	strcpy(buf1, a->group);
	if (f) for (g = 0; g < f; g++) {
		if ((s = strchr(buf1, '.')) == NULL) g = f;
		  else *s = '/';
	}
	if ((s = strchr(buf1, '.')) != (char *)NULL) *s = '\0';
	sprintf(buf2, "%s/%s", OLDNEWS, buf1);
	if (!e_debug && stat(buf2, &st) < 0) {
		if (wmkdir(buf2, 0755) < 0) {
			logmsg(LOGERR, "cannot mkdir(%s)\n", buf2);
			return((char *)NULL);
		} else {
			if (verbose > 0)
				logmsg(LOGINFO, "created dir %s\n", buf2);
		}
	}
	if (s == (char *)NULL) break;
  }

  /*
   * Allright we have it, now put article in it and/or update archive.
   * This piece of code _assumes_ that we can just simply LINK the old
   * articles to their new place in the file system.  FIXME: - FvK
   */
  sprintf(seq, "%d", art->seq);
  if (archc == 0) {
	sprintf(buf1, "%s/%s", buf2, seq);
	if (!e_debug && link(seq, buf1) < 0) {
		logmsg(LOGERR, "can't link(%s, %s).\n", seq, buf1);
		return((char *)NULL);
	}
	if (e_debug) logmsg(LOGINFO, "expire: link(%s, %s)\n", seq, buf1);
  } else {	/* use archive program */
	sprintf(buf1, "%s/archive", buf2);
	for(f = 0; f < archc; f++) {
		args[f] = archv[f];
		if (strcmp("%f", args[f]) == 0) args[f] = buf1;
		if (strcmp("%a", args[f]) == 0) args[f] = seq;
		if (strcmp("%s", args[f]) == 0) args[f] = art->subject;
	}
	args[f] = (char *)NULL;

	if (e_debug) {
		strcpy(buf2, args[0]);
		for(f = 1; f < 16 && args[f]; f++) {
			strcat(buf2, " \"");
			strcat(buf2, args[f]);
			strcat(buf2, "\"");
		}
		logmsg(LOGINFO, "expire: archive %s\n", buf2);
		return(buf1);
	}

	/* Fork and execute. */
	if ((pid = fork()) == 0) {
		(void) execv(args[0], args);
		exit(1);
	} else if (pid > 0) {
		while (wait(&status) != pid)
			    ;
	}

	/* Some archivers leave an "archive.bak" behind. Throw it away. */
	sprintf(buf2, "%s.bak", buf1);
	(void) unlink(buf2);
	
	if (pid < 0 || WEXITSTATUS(status) != 0) {
		logmsg(LOGERR, "error in executing %s\n", args[0]);
		return((char *)NULL);
	}
	
  }
  return(buf1);
}


/* Walk through newsgroups. */
void
walk() 
{
  char *s;
  char *arch_name;
  char temp[256];
  char directory[512];
  DIR *dir;
  struct dirent *entry;
  struct stat st;
  ART *artlist, *prevart, *art;
  time_t t;
  int delete;
  int updated;
  int first, last;
  int artnum;
  int group_ok, arch_ok;
  off_t org_vol, del_vol, tot_del_vol;
  off_t bytes;
  ACT *a;
 
  (void) time(&t);
  tot_del_vol = 0L;
  
  for(a = active; a; a = a->next) {

	group_ok = grouplist ? group_match(a->group, grouplist) : 1;
	arch_ok  = archlist  ? group_match(a->group, archlist)  : 0;

	if (e_debug || verbose > 2) {
		if (group_ok) logmsg(LOGINFO, "expire: examining %s\n",
								a->group);
		  else logmsg(LOGINFO, "expire: skipping %s\n", a->group);
	}

	/* See if this matches the groups on the command line. */
	if (! group_ok) continue;

	sprintf(directory, "%s/%s", SPOOLDIR, gtof(a->group));
	dir = opendir(directory);
	if (dir == (DIR *)NULL) {
		if (verbose > 1) {
			logmsg(LOGINFO, "%s does not have a directory (no error).\n",
							a->group);
		}
		continue;
	}
	(void) chdir(directory);

	/* Reset some variables. */
	org_vol = 0L;
	del_vol = 0L;
	artlist = (ART *)NULL;
	prevart = (ART *)NULL;
	updated = FALSE;

	/* Walk through directory */
	while((entry = readdir(dir)) != (struct dirent *)0) {
		
		/* Fill in some variables */
		if ((artnum = atoi(entry->d_name)) == 0) continue;
		delete = 0;
		arch_name = (char *)NULL;
		if (stat(entry->d_name, &st) < 0 || 
		    st.st_mode & S_IFMT != S_IFREG) continue;
		bytes = (1 + (st.st_size / BLOCK_SIZE)) * BLOCK_SIZE;
		org_vol += bytes;

		/* Now malloc an art struct and link it in. */
		if ((art = (ART *)malloc(sizeof(ART))) == (ART *)NULL) {
			logmsg(LOGERR, "out of memory\n");
			exit(1);
		}
		art->seq = artnum;
		art->next = (ART *)NULL;

		examine(a, art, entry->d_name, &st, t);
		if (usefrom) {
			if (strchr(usefrom, '@') == (char *)NULL) {
				s = strchr(art->from, '@');
				if (s == (char *)NULL)  s = art->from;
				  else s++;
			} else s = art->from;

			if (strcmp(s, usefrom) == 0) {
				delete++;
			}
		} else {
			if (usepostdate &&
			    art->art_days >= expiretime) {
				delete++;
			} else if (art->stat_days >= expiretime) {
				delete++;
			}
		}
		if (delete) {
			if (arch_ok)
				if ((arch_name = store(art, a)) ==
					(char *)NULL) delete = 0;
			if (!e_debug && unlink(entry->d_name) < 0) {
				logmsg(LOGERR,
				" WARNING!! %s/%s NOT DELETED\n",
					directory, entry->d_name);
				delete = 0;
			} else {
				del_vol += bytes;
				updated = TRUE;
				if (verbose > 1) {
				    if (arch_name) sprintf(temp,
					" A %s", arch_name);
				    else strcpy(temp, "");
				    logmsg(LOGINFO, "D %s/%s %s\n",
					a->group, entry->d_name, temp);
				}
			}
		}
		/* If deleted, don't link it into the list. */
		if (delete) free(art);
		  else {
			/* Link it into the list */
			art->next = artlist;
			artlist = art;
		}
	}
	(void) closedir(dir);

	/* Report cumutated sizes. */
	tot_del_vol += del_vol;
	if (updated && verbose)
		logmsg(LOGINFO, "%s: volume was %ld and is now %ld\n",
			a->group, org_vol, org_vol - del_vol);


	/* Now find the first and the last article, freeing the list. */
	first = 0;
	last = 0;
	for(art = artlist; art; art = prevart) {
		prevart = art->next;
		if (art->seq > last) last = art->seq;
		if (art->seq < first || first == 0) first = art->seq;
		free(art);
	}

	/* Or everything was deleted... */
	if (artlist == (ART *)NULL) {
		first = a->last + 1;
		last  = a->last;
	}

	if (updated && !e_debug && a->first != first) {
		/* Update the active file too. */
		if (get_group(a->group, MAKE_LOCK) == (ACT *)NULL) {
			logmsg(LOGERR, "active file error - quitting\n");
			exit(1);
		}
		a->first = first;
		act_upd(a);
	}
	if ((e_debug || verbose > 2) && updated) {
		if (a->first != first)
			logmsg(LOGINFO, "active update: %s %d %d %c\n",
				a->group, last, first, a->type);
		  else
			logmsg(LOGINFO,
			  "active file not updated, lowest is %d\n", first);
	}
  }
  if (verbose) logmsg(LOGINFO, "deleted %ld bytes\n", tot_del_vol);
}


/* Trap signals (SIGINT and SIGPIPE).  Useful for e_debugging purposes. */
void
trap(sig)
int sig;
{
  (void) signal(SIGINT, SIG_IGN);
  (void) signal(SIGPIPE, SIG_IGN);
  
  logmsg(LOGERR, "received signal %d. Goodbye!\n", sig);
  exit(1);
}


void
usage()
{
  verbose = e_debug = 0;
  fprintf(stderr, "Usage: expire [-d] [-n groups] [-a [groups]] [-e days]\n");
  fprintf(stderr, "       [-t archiver] [-v level] [-f user] [-p]\n");
  logmsg(LOGERR, "argument format error\n");
  exit(1);
}


/*
 * Main part of the program.
 * Process options and call appropriate functions.
 */
int
main(argc, argv)
int argc;
char **argv;
{
  int c;
  
  if ((program = strrchr(argv[0], '/')) == (char *)NULL) program = argv[0];
    else program++;

  (void) signal(SIGINT, trap);
  (void) signal(SIGPIPE, trap);
  
  if (isatty(0)) {
	debug = 1;
	verbose = 1;
  }

#ifndef COHERENT
  opterr = 0;
#endif
  while((c = getopt(argc, argv, "t:dn:e:a:v:pf:")) != EOF) {
	switch(c) {
		case 't':  /* specify archiver */
			opt_t(optarg);
			break;

		case 'd':  /* e_debug: only report */
			verbose = 3;
			debug = 1;
			e_debug++;
			break;

		case 'n':  /* specify newsgroups */
			grouplist = optarg;
			break;

		case 'e': /* specify expire time */
			if ((expiretime = atoi(optarg)) == 0) errflg++;
			break;

		case 'a': /* specify groups to arcive */
			archlist = optarg;
			break;

		case 'v': /* set verbose mode */
			if ((verbose = atoi(optarg)) == 0 &&
			     optarg[0] != '0') errflg++;
			if (verbose < 0) verbose = 0;
			if (verbose > 3) verbose = 3;     
			debug = 1;
			break;

		case 'p': /* use posting date of article */
			usepostdate++;
			break;

		case 'f': /* remove messages send by specific user */
			usefrom = optarg;
			break;

		default:
			errflg++;
			break;
	}
	if (errflg) usage();
  }

  if (optind < argc) usage();

  if ((active = act_rd()) == (ACT *)NULL) exit(1);

  walk();
  
#if HAVE_HIST
  if (!e_debug) do_history();
#endif

  exit(0);
}
