/*
  fastcancel.c - locally cancel articles
  written by Olaf Titz, July 1997 as part of c-nocem.
  Public domain.

  This program takes on stdin a list of Message-IDs and does local
  cancels on them. I.e. if the article is in the history, it is
  deleted; if not, it gets recorded in the history.
  A log entry will be put on stdout for every ID processed.

  This program manages the history database directly. For C News, call
  it with LOCK set. For INN, call it with the server paused.

  Arguments:
  -d            Debug. Don't do anything, just print what to do.
  -i            Don't set dbzincore (default: set).
  -f logformat  Use format string for logging. In this string, a '+'
                sign is replaced with '-' for deleted articles and a
		'#' sign is replaced with the Message-ID.
		Empty string = don't log
  -h histfile   Use the specified history file.
  -s spooldir   Use the specified spool directory.
  -l            Log statistics.
  -c	 	Generate a canonical cancel Message-ID too.
		(Thanks to Wolfgang Zenker for the suggestion.)
  -r		Only remove articles, don't add anything.
  -a num        Write file names for fastrm to given descriptor
                instead of removing articles.

*/
static char *RCSID="$Id: fastcancel.c,v 1.12 2001/02/16 21:59:39 olaf Exp $";

#ifdef INN2
#include "cncdbz.h"
#else
#include <stdio.h>
#include <stdlib.h>
#include <syslog.h>
#include <time.h>
#include <unistd.h>
/* mystring.h conflicts with string.h */
#include <string.h>
#include "dbz.h"
#endif

#ifdef DBZ_REVERTED
/* INN 2.3 changed the rules _again_ and now uses the same API for standard
   dbz as for tagged hash. */
#define DO_TAGGED_HASH
#endif

#define MAXLINE 10000
#ifndef LOGLEVEL
#define LOGLEVEL LOG_NOTICE
#endif

static char logform0[]="+ #";		/* logging format */
static char *logform=logform0,
    *logmark=logform0,
    *logform2="";

static FILE *hf;              		/* history file */
static int debug=0, cancelid=0, rmonly=0;
static int sdela=0, sdelf=0, sadda=0;	/* statistics */
static int adesc=-1;                    /* fastrm file descriptor */

/* where to print history lines */
#define HF (debug ? stdout : hf)

static void usage(const char *p)
{
    fprintf(stderr,
	    "usage: %s [-d] [-i] [-l] [-c] [-r] [-f logformat] [-h histfile] [-s spooldir] [-a descriptor]\n",
	    p);
    exit(1);
    /* NOTREACHED */
}

/* Put out a log entry. */
static void logemit(char m, const char *mid)
{
    if (!*logform)
	return;
    *logmark=m;
    printf("%s%s%s\n", logform, mid, logform2);
}

/* Process a new (not in history) Message-ID. */
static void do_new(const char *mid
#ifdef INN2
                   , HASH hash
#endif
    )
{
#ifdef INN2
#ifndef DO_TAGGED_HASH
    void *iv;
    idxrec ov;
#ifdef HAVE_INNCONF_EXTENDEDDBZ
    idxrecext ev;
#endif
#endif
#else
    datum k, v;
#endif
    long p=-1; /* only used in if(!debug) branches */
    time_t t=time(0);

    if (rmonly)
	return;

    if (!debug) {
	/* prepare to store in dbz */
	if (fseek(hf, 0, SEEK_END)<0) {
	    perror("do_new: fseek");
	    return;
	}
	if ((p=ftell(hf))<0) {
	    perror("do_new: ftell");
	    return;
	}
    }

    /* generate a history entry */
#ifdef INN2
#if defined(HAVE_INNCONF_STOREMSGID) && defined(HAVE_INNCONF_STORAGEAPI)
    if (innconf->storageapi || !innconf->storemsgid)
        fprintf(HF, "[%s]\t%lu~-~%lu\t\n", HashToText(hash), t, t);
    else
        fprintf(HF, "%s\t%lu~-~%lu\t\n", mid, t, t);
#else
    /* old INN 2.0, new INN 2.3 */
    fprintf(HF, "[%s]\t%lu~-~%lu\t\n", HashToText(hash), t, t);
#endif
#else
    /* classic */
    fprintf(HF, "%s\t%lu~-\n", mid, t);
#endif

    if (!debug) {
        if (fflush(hf)) {
            perror("fflush");
            return;
        }

#ifdef INN2
#ifdef DO_TAGGED_HASH
        /* store in dbz - INN2, tagged hash */
        if (dbzstore(hash, p) == DBZSTORE_ERROR)
            perror("dbzstore");
#else
        /* store in dbz - INN2, no tagged hash */
#ifdef HAVE_INNCONF_EXTENDEDDBZ
        if (innconf->extendeddbz) {
            ev.offset[HISTOFFSET]=p;
            ev.offset[OVEROFFSET]=0;
            ev.overindex=OVER_NONE;
            ev.overlen=0;
            iv=&ev;
        } else
#endif
        {
            ov.offset=p;
            iv=&ov;
        }
        if (dbzstore(hash, iv) == DBZSTORE_ERROR)
            perror("dbzstore");
#endif
#else
        /* store in dbz - classical */
	k.dptr=(char *)mid;
	k.dsize=strlen(mid)+1;
	v.dptr=(char *)&p;
	v.dsize=sizeof(p);
	if (dbzstore(k, v)<0)
	    perror("do_new: dbzstore");
#endif
    }
    logemit('+', mid);
    ++sadda;
}

/* Process an old history line. */
static void do_old(char *lin, const char *mid)
{
    char *p, *q;

    /* skip ID and date fields */
    (void) strtok(lin, "\t\n");
    (void) strtok(NULL, "\t\n");
    p=strtok(NULL, "\t \n");

    /* get at the file names */
#ifdef INN2
    if (p
#ifdef HAVE_INNCONF_STORAGEAPI
        && innconf->storageapi
#endif
        ) {
        if (debug)
            printf("remove %s\n", p);
        else
            if (adesc>=0) {
                (void) write(adesc, p, strlen(p));
                (void) write(adesc, "\n", 1);
            } else {
                (void) SMcancel(TextToToken(p));
            }
        logemit('-', mid);
        ++sdela;
        return;
    }
#endif
    while (p) {
	for (q=p; *q; ++q)
	    if (*q=='.')
		*q='/';
	if (debug)
	    printf("unlink %s\n", p);
	else
	    if (adesc>=0) {
		(void) write(adesc, p, strlen(p));
		(void) write(adesc, "\n", 1);
	    } else {
		(void) unlink(p);
	    }
	++sdelf;
	p=strtok(NULL, "\t \n");
    }
    logemit('-', mid);
    ++sdela;
}

#ifdef INN2
#ifdef HAVE_DBZFETCH_2
#define DBZfetch(k,v) dbzfetch(k,v)
#else
static BOOL DBZfetch(const HASH key, OFFSET_T *value)
{
    OFFSET_T val = dbzfetch(key);
    if (val==(OFFSET_T)-1)
        return FALSE;
    *value=val;
    return TRUE;
}
#endif
#endif

/* Process one Message-ID. */
static void do_one(const char *mid, int old)
{
#ifdef INN2
    HASH k;
#ifndef DO_TAGGED_HASH
    idxrec ov;
#ifdef HAVE_INNCONF_EXTENDEDDBZ
    idxrecext ev;
#endif
#endif
#else
    datum k, v;
#endif
    long p;
    char lbuf[MAXLINE];

#ifdef INN2
    /* look it up in history: INN2 */
    if (*mid=='[')
        k=TextToHash(mid+1);
    else
        k=HashMessageID(mid);
#ifdef DO_TAGGED_HASH
    if (!DBZfetch(k, &p)) {
        do_new(mid, k);
        return;
    }
#else
    /* look it up in history: INN2, no tagged hash  */
#ifdef HAVE_INNCONF_EXTENDEDDBZ
    if (innconf->extendeddbz) {
	if (!DBZfetch(k, &ev)) {
            do_new(mid, k);
            return;
	}
	p=ev.offset[HISTOFFSET];
    } else
#endif
    {
	if (!DBZfetch(k, &ov)) {
            do_new(mid, k);
            return;
	}
	p=ov.offset;
    }
#endif
#else
    /* look it up in history: classic dbz */
    k.dptr=(char *)mid;
    k.dsize=strlen(mid)+1;
    v=dbzfetch(k);
    if (!v.dptr) {
	do_new(mid);
        return;
    }
    if (v.dsize!=sizeof(p)) {
        /* fatal error */
        fprintf(stderr, "dbz: invalid dsize %d", v.dsize);
        return;
    }
    p=*((long *)v.dptr);
#endif
    if (!old)
        /* if the "cancel" ID is already there, do nothing */
        return;
    /* retrieve the actual history line */
    if (fseek(hf, p, SEEK_SET)<0) {
        perror("do_one: fseek");
        return;
    }
    if (!fgets(lbuf, sizeof(lbuf), hf)) {
        perror("do_one: fgets");
    }
    if (lbuf[strlen(lbuf)-1]!='\n')
        fprintf(stderr, "warning: %s long history line\n", mid);
    do_old(lbuf, mid);
}

extern char *optarg;

int main(int argc, char *argv[])
{
    char buf[MAXLINE]="<cancel.";
    #define BUFO 7
    int c0, c1=1, i=1, l=0;
    char *h=HISTFILE;
    char *p=NEWSARTS;

#ifdef INN2
    if (ReadInnConf() < 0) {
        perror("main: ReadInnConf");
        exit(1);
    }
#endif
    while ((c0=getopt(argc, argv, "dilcrf:h:s:a:"))!=EOF)
	switch(c0) {
	case 'd': debug=1; break;
	case 'i': i=0; break;
	case 'l': l=1; break;
	case 'c': cancelid=1; break;
	case 'f': logform=optarg; break;
	case 'h': h=optarg; break;
	case 's': p=optarg; break;
	case 'r': rmonly=1; break;
	case 'a': adesc=atoi(optarg); break;
	default: usage(argv[0]);
	}
    if (chdir(p)<0) {
	perror("main: chdir");
	exit(1);
    }
    hf=fopen(h, debug ? "r" : "r+");
    if (!hf) {
	perror("main: fopen");
	exit(1);
    }

#ifdef INN2
#define dbminit dbzinit
    if (
#ifdef HAVE_INNCONF_STORAGEAPI
        innconf->storageapi &&
#endif
        adesc<0 && !debug) {
        BOOL v=TRUE;
        if (!SMsetup(SM_RDWR, &v)) {
            perror("main: SMsetup");
            exit(1);
        }
        if (!SMinit()) {
            perror("main: SMinit");
            exit(1);
        }
    }
#else
    dbzincore(i);
#endif
    if (dbminit(h)<0) {
	perror("main: dbmopen");
	exit(1);
    }

    /* parse the lame attempt at a format string */
    for (p=logform; *p; ++p)
	switch(*p) {
	case '+': logmark=p; break;
	case '#': *p='\0'; logform2=p+1; break;
	}

    while (fgets(buf+BUFO, sizeof(buf)-BUFO, stdin)) {
	c0=c1;
	c1=((buf+BUFO)[strlen(buf+BUFO)-1]=='\n');
	if (!c0)
	    continue; /* skip continuation bogosities */
	if (!c1) {
	    fprintf(stderr, "stdin: long line\n");
	    continue;
	}
	/* Kill evil characters in MID */
	(buf+BUFO)[strcspn(buf+BUFO, " \t\n")]='\0';
	/* first syntax check */
	if (buf[BUFO]!='<' || (buf+BUFO)[strlen(buf+BUFO)-1]!='>') {
#ifdef INN2
            if (buf[BUFO]!='[' || (buf+BUFO)[strlen(buf+BUFO)-1]!=']') {
#endif
                fprintf(stderr, "stdin: syntax error\n");
                continue;
#ifdef INN2
            }
#endif
        }
	do_one(buf+BUFO, 1);
	if (cancelid && (strncmp(buf+BUFO, buf, BUFO)!=0)) {
	    /* check for <cancel...> ID, if original
	       ID is not cancel itself */
	    buf[BUFO]='.';
	    do_one(buf, 0);
	}
    }
    if (l) {
	openlog("fastcancel", LOG_NDELAY, LOG_NEWS);
	syslog(LOGLEVEL, "deleted %d arts %d files, added %d arts",
	       sdela, sdelf, sadda);
    }
    exit(0);
}
