/*
 * DCANCEL.C	- cancel articles and rewrite spool files.
 *
 * (c)Copyright 2003, Russell Vincent, All Rights Reserved.  Refer to
 *    the COPYRIGHT file in the base directory of this distribution 
 *    for specific rights granted.
 *
 */

#include "defs.h"

void CancelReader(char *msgid);
void AddToSpoolCancel(History *h);
void RewriteFiles(void);
void PrintCancelList(void);

typedef struct HistoryList {
    History h;
    struct HistoryList *nextent;
    struct HistoryList *nextspool;
    struct HistoryList *nextdir;
    struct HistoryList *nextfile;
} HistoryList;

int VerboseOpt = 1;
int ForReal = 1;
int HistoryCancel = 0;
int ReaderCancel = 0;
int SpoolCancel = 0;
HistoryList *CancelList = NULL;
int CancelCount = 0;
int RewriteCount = 0;
int RemoveCount = 0;
int ReaderCount = 0;
int UnExpire = 0;

void
Usage(void)
{
	printf("This program performs a cancel on a set of msgid's\n");
	printf("dcancel -h|-s [-u] [-v]\n");
	printf("\t-a\tstandard cancel (history and spool)\n");
	printf("\t-h\tcancel articles in feeder history\n");
	printf("\t-s\tcancel articles in feeder spool - not recommended\n");
	printf("\t-u\tuncancel history entries\n");
	printf("\t-v\tbe more verbose\n");
	printf("\t-C file\tspecify diablo.config to use\n");
	printf("\t-d[n]\tset debug [with optional level]\n");
	printf("\t-V\tprint version and exit\n");
	printf("\n");
	printf("By default, the list of msgid's is read from stdin\n");
	exit(0);
}


int
main(int ac, char **av)
{
    int n;
    char buf[32768];
    char *msgid;
    FILE *inputFile = NULL;
    History h;

    DebugOpt = 1;
    LoadDiabloConfig(ac, av);

    for (n = 1; n < ac; ++n) {
	char *ptr = av[n];

	if (*ptr == '-') {
	    ptr += 2;
	    switch(ptr[-1]) {
	    case 'a':
		HistoryCancel = 1;
		SpoolCancel = 1;
		break;
	    case 'h':
		HistoryCancel = 1;
		break;
	    case 'n':
		ForReal = 0;
		break;
#if 0
	    case 'r':
		ReaderCancel = 1;
		break;
#endif
	    case 's':
		SpoolCancel = 1;
		break;
	    case 'u':
		UnExpire = 1;
		break;
	    case 'v':
		VerboseOpt = (*ptr) ? strtol(ptr, NULL, 0) : 1;
		break;
	    case 'C':		/* parsed by LoadDiabloConfig */
		if (*ptr == 0)
		    ++n;
		break;
	    case 'd':
		VerboseOpt++;
		if (isdigit((int)(unsigned char)*ptr)) {
		    DebugOpt = strtol(ptr, NULL, 0);
		} else {
		    --ptr;
		    while (*ptr == 'd') {
			++DebugOpt;
			++ptr;
		    }
		}
		break;
	    case 'V':
		PrintVersion();
		break;
	    default:
		fprintf(stderr, "Illegal option: %s\n", ptr - 2);
		exit(1);
	    }
	} else {
	    fprintf(stderr, "Illegal argument: %s\n", ptr);
	    exit(1);
	}
    }

    /*
     * this isn't an error, but a request to list 
     * valid arguments, then exit.
     */

    if (ac == 1)
	Usage();
    if (!HistoryCancel && !ReaderCancel && !SpoolCancel)
	Usage();
    if (HistoryCancel || SpoolCancel) {
	HistoryOpen(NULL, 0);
	LoadSpoolCtl(0, 1);
    }

    if (inputFile == NULL)
	inputFile = stdin;
    while (fgets(buf, sizeof(buf), inputFile) != NULL) {
	msgid = buf;
	while (isspace((int)*msgid))
	    msgid++;
	strtok(msgid, " \t\n");
	if (HistoryCancel) {
	    if (HistoryExpire(msgid, &h, UnExpire)) {
		if (h.iter == (uint16)-1) {
		    fprintf(stderr, "Already removed %s\n", msgid);
		    continue;
		}
		CancelCount++;
		if (VerboseOpt)
		    printf("%sCancelled history entry for: %s\n",
					UnExpire ? "Un" : "", msgid);
		if (SpoolCancel)
		    AddToSpoolCancel(&h);
	    } else {
		printf("No history entry for: %s\n", msgid);
	    }
	} else if (SpoolCancel) {
	    if (HistoryLookup(msgid, &h) == 0)
		AddToSpoolCancel(&h);

	}
	if (ReaderCancel)
	    CancelReader(msgid);
    }
    PrintCancelList();
    if (SpoolCancel)
	RewriteFiles();
    if (VerboseOpt)
	printf("Rewrote %d spool files with %d articles removed\n",
						RewriteCount, RemoveCount);
    exit(0);
}

/*
 * Create a linked list of spool objects, directories and filenames
 * so that we can work on all entries in one file at a time
 */
void
AddToSpoolCancel(History *h)
{
    HistoryList *hl;
    HistoryList *thl;
    int spool = H_SPOOL(h->exp);
    int dir = h->gmt;
    int f = h->iter;

    hl = (HistoryList *)malloc(sizeof(HistoryList));
    if (hl == NULL) {
	fprintf(stderr, "malloc error: %s\n", strerror(errno));
	exit(1);
    }
    bzero(hl, sizeof(HistoryList));
    hl->h = *h;
    if (CancelList == NULL) {
	CancelList = hl;
	return;
    }
    thl = CancelList;
    while (thl->nextspool != NULL && H_SPOOL(thl->h.exp) != spool)
	thl = thl->nextspool;
    if (H_SPOOL(thl->h.exp) != spool) {
	thl->nextspool = hl;
	return;
    }
    while (thl->nextdir != NULL && thl->h.gmt != dir)
	thl = thl->nextdir;
    if (thl->h.gmt != dir) {
	thl->nextdir = hl;
	return;
    }
    while (thl->nextfile != NULL && thl->h.iter != f)
	thl = thl->nextfile;
    if (thl->h.iter != f) {
	thl->nextfile = hl;
	return;
    }
    while (thl->nextent != NULL)
	thl = thl->nextent;
    thl->nextent = hl;
}

void
PrintCancelList(void)
{
    HistoryList *hl_sp;
    HistoryList *hl_dir;
    HistoryList *hl_file;
    HistoryList *hl;

    for (hl_sp = CancelList; hl_sp != NULL; hl_sp = hl_sp->nextspool) {
	printf("SPOOL: %d\n", H_SPOOL(hl_sp->h.exp));
	for (hl_dir = hl_sp; hl_dir != NULL; hl_dir = hl_dir->nextdir) {
	    printf(" DIR: D.%08x\n", hl_dir->h.gmt);
	    hl_file = hl_dir;
	    while (hl_file != NULL) {
		printf("   FILE: B.%04x\n", hl_file->h.iter);
		hl = hl_file;
		while (hl != NULL) {
		    PrintHistory(&hl->h);
		    hl = hl->nextent;
		}
		hl_file = hl_file->nextfile;
	    }
	}
    }
}

void
CancelReader(char *msgid)
{
}

char *
getMsgId(int fd, char *buf, int hdrLen)
{
    static char msgid[MAXMSGIDLEN + 1];
    char *p;
    char *q;
    int n;

    bzero(msgid, sizeof(msgid));
    p = buf;
    while (p - buf < hdrLen - 15 && (*p != '\n' ||
				strncasecmp(p, "\nMessage-ID:", 12) != 0))
	p++;
    if (strncasecmp(p, "\nMessage-ID:", 12) != 0) {
	fprintf(stderr, "Cannot find Message-ID header!\n");
	return(NULL);
    }
    p += 12;
    while (isspace(*p))
	p++;
    if (*p != '<') {
	fprintf(stderr, "Invalid Message-ID header!\n");
	return(NULL);
    }
    q = p;
    while (q - buf < hdrLen && *q != '>')
	q++;
    if (*q != '>') {
	fprintf(stderr, "Invalid Message-ID header!\n");
	return(NULL);
    }
    n = q - p + 1;
    if (n >= sizeof(msgid))
	n = sizeof(msgid) - 1;
    strncpy(msgid, p, n);
    return(msgid);
}

/*
 * Map an article into memory, using xmap() if it is not compressed or
 * allocating/reallocating a buffer for it if it is compresssed
 *
 * Returns:
 *	pointer	Success
 *	NULL	Failure
 */
char *
MapArticle(int fd, char *fname, off_t offset, uint32 size, int exp, int *artSize, int *compressed, FILE *logFo)
{
    static char *mmapBase = NULL;
    static off_t mmapLen = 0;

    if (mmapBase != NULL)
	xunmap(mmapBase, mmapLen);

    *artSize = 0;
    *compressed = 0;

    if (SpoolCompressed(H_SPOOL(exp))) {
#ifdef X_USE_ZLIB
	static char *base = NULL;
	static off_t baseLen = 0;

	gzFile *gzf;
	SpoolArtHdr tah = { 0 };
	char *p;

	lseek(fd, offset, 0);
	if (read(fd, &tah, sizeof(SpoolArtHdr)) != sizeof(SpoolArtHdr)) {
	    close(fd);
	    fprintf(logFo, "Unable to read article header (%s)\n",
							strerror(errno));
	    return(NULL);
	}
	if ((uint8)tah.Magic1 != STORE_MAGIC1 &&
					(uint8)tah.Magic2 != STORE_MAGIC2) {
	    lseek(fd, h->boffset, 0);
	    tah.Magic1 = STORE_MAGIC1;
	    tah.Magic2 = STORE_MAGIC2;
	    tah.HeadLen = sizeof(tah);
	    tah.ArtLen = h->bsize;
	    tah.ArtHdrLen = h->bsize;
	    tah.StoreLen = h->bsize;
	}
	gzf = gzdopen(fd, "r");
	if (gzf == NULL) {
	    fprintf(logFo, "Error opening compressed article\n");
	    return(NULL);
	}
	if (base == NULL || baseLen < tah.HeadLen + tah.ArtLen + 2) {
	    baseLen = tah.ArtLen + tah.HeadLen + 2;
	    base = (char *)realloc(base, baseLen);
	    if (base == NULL) {
		fprintf(logFo, "Unable to malloc %d bytes for article (%s)\n",
						baseLen, strerror(errno));
		gzclose(gzf);
		return(NULL);
	    }
	}
	p = base;
	*p++ = 0;
	bcopy(&tah, p, tah.HeadLen);
	p += tah.HeadLen;
	if (gzread(gzf, p, tah.ArtLen) != tah.ArtLen) {
	    fprintf(logFo, "Error uncompressing article\n");
	    return(NULL);
	}
	p[tah.ArtLen] = 0;
	*artSize = tah.ArtLen + tah.HeadLen;
	*compressed = 1;
	gzclose(gzf);
#else
        fprintf(logFo, "Compressed spools not yet supported with dcancel\n");
#endif
    } else {
	mmapLen = size;
	mmapBase = xmap(NULL, mmapLen, PROT_READ, MAP_SHARED, fd, offset);
	if (mmapBase != NULL) {
	    *artSize = size;
	    *compressed = 0;
	} else {
	    fprintf(logFo, "Unable to map file %s: %s (%llu,%u)\n",
					fname, strerror(errno), offset, size);
	 }
	return(mmapBase);
    }
    return(NULL);
}

void
rewriteFile(HistoryList *hl_file)
{
    int oldf;
    int newf = -1;
    static char *artBuf = NULL;
    int n;
    int eof = 0;
    char opath[PATH_MAX];
    char npath[PATH_MAX];
    SpoolArtHdr artHdr;
    off_t filepos;
    int opened = 0;
    HistoryList *hl;
    History h;
    int copyArt;
    char *msgid;
    int artSize;
    int compressed;
    hash_t hv;
    
    /*
     * Get the article filename and open it
     */
    ArticleFileName(opath, sizeof(opath), &hl_file->h, ARTFILE_FILE_REL);

    printf("Rewriting: %s\n", opath);

    if ((oldf = open(opath, O_RDONLY)) == -1) {
	fprintf(stderr, "Cannot open %s : %s\n", opath, strerror(errno));
	return;
    }

    /*
     * Find a new spool filename
     */
    n = 0;
    while (!opened) {
	hl_file->h.iter++;
	hl_file->h.iter &= 0x7FFF;
	ArticleFileName(npath, sizeof(npath), &hl_file->h, ARTFILE_FILE_REL);
	if ((newf = open(npath, O_RDWR|O_CREAT|O_EXCL, 0644)) == -1) {
	    if (++n > 200) {
		fprintf(stderr, "Cannot create new spool file %s : %s\n",
							npath, strerror(errno));
		return;
	    }
	    continue;
	}
	opened = 1;
    }

    /*
     * Copy all the articles except the ones at the specified locations
     */
    filepos = 0;
    while (!eof) {
	lseek(oldf, filepos, SEEK_SET);
	/*
	 * Read the article's spool header
	 */
	n = read(oldf, &artHdr, sizeof(artHdr));
	if (n == 0) {
	    eof = 1;
	    break;
	}
	if (n != sizeof(artHdr)) {
	    fprintf(stderr, "Error reading article header at %lld in %s\n",
							filepos, opath);
	    break;
	}
	if (artHdr.Magic1 != STORE_MAGIC1 || artHdr.Magic2 != STORE_MAGIC2) {
	    fprintf(stderr, "Invalid article header at %lld (%x.%x)in %s\n",
				filepos, artHdr.Magic1, artHdr.Magic2, opath);
	    break;
	}

	/*
	 * Map the article into memory (including the artHdr)
	 */
	artBuf = MapArticle(oldf, opath, filepos,
				artHdr.StoreLen,
				SpoolCompressed(H_SPOOL(hl_file->h.exp)),
				&artSize, &compressed, stderr);
	if (artBuf == NULL)
	    break;

	if (compressed) {
	    fprintf(stderr, "Compressed spools are currently not supported\n");
	    break;
	}

	/*
	 * Extract the Message-ID from the article headers
	 */
	msgid = getMsgId(oldf, artBuf, artHdr.ArtHdrLen);
	if (msgid == NULL)
	    break;
	hv = hhash(msgid);

	/*
	 * Lookup the msgid in history for later modification
	 */
	if (HistoryCancel && HistoryLookup(msgid, &h) != 0) {
	    fprintf(stderr, "Cannot find %s in history - skipping\n",
								msgid);
	    filepos += artSize;
	    continue;
	}


	/*
	 * Copy the article unless the offset matches the history entry
	 * for the skipped article(s)
	 */
	copyArt = 1;
	for (hl = hl_file; hl != NULL; hl = hl->nextent) {
	    if (hl->h.hv.h1 == hv.h1 && hl->h.hv.h2 == hv.h2)
		copyArt = 0;
	}
	if (copyArt) {
	    h.boffset = lseek(newf, 0, 1);
	    printf("Copying article at offset %lld to offset %d\n",
							filepos, h.boffset);
	    if (compressed) {
		lseek(oldf, filepos, SEEK_SET);
		read(oldf, artBuf, artHdr.StoreLen);
		if (write(newf, &artBuf, artHdr.StoreLen) != artHdr.StoreLen) {
		    fprintf(stderr, "Error writing article in %s (%s)\n",
							npath, strerror(errno));
		    break;
		}
	    } else {
		if (write(newf, &artBuf, artSize) != artSize) {
		    fprintf(stderr, "Error writing article in %s (%s)\n",
							npath, strerror(errno));
		    break;
		}
	    }
	    filepos += artSize;
	} else {
	    printf("Skipping article at offset %lld\n", filepos);
	    filepos += artHdr.StoreLen;
	    lseek(oldf, filepos, SEEK_SET);
	    h.iter = (uint16)-1;
	    h.boffset = 0;
	    h.bsize = 0;
	    h.gmt = 0;
	    h.exp |= EXPF_EXPIRED;
	    RemoveCount++;
	}

	/*
	 * Update the history entry with the new location or cancel
	 */
	if (HistoryCancel && ForReal)
	    HistoryStore(&h);
    }
    close(oldf);
    close(newf);
    if (eof == 1) {
	RewriteCount++;
	if (ForReal) {
	    struct stat sb;
	    if (stat(npath, &sb) == 0 && sb.st_size == 0) {
		remove(opath);
		remove(npath);
	    } else {
		rename(npath, opath);
	    }
	}
    } else {
	if (ForReal)
	    remove(npath);
    }
}

void
RewriteFiles(void)
{
    HistoryList *hl_spool;
    HistoryList *hl_dir;
    HistoryList *hl_file;
    HistoryList *hl;

    if (chdir(PatExpand(SpoolHomePat)) == -1) {
	fprintf(stderr, "Unable to chdir(%s): %s\n",
				PatExpand(SpoolHomePat), strerror(errno));
	exit(1);
    }
    if (VerboseOpt)
	printf("Rewriting spool file(s) for cancelled articles\n");

    hl_spool = CancelList;
    while (hl_spool != NULL) {
	printf("SPOOL: %d\n", H_SPOOL(hl_spool->h.exp));
	hl_dir = hl_spool;
	while (hl_dir != NULL) {
	    printf(" DIR: D.%08x\n", hl_spool->h.gmt);
	    hl_file = hl_dir;
	    while (hl_file != NULL) {
		printf("   FILE: B.%04x\n", hl_spool->h.iter);
		hl = hl_file;
		while (hl != NULL) {
		    PrintHistory(&hl->h);
		    hl = hl->nextent;
		}
		rewriteFile(hl_file);
		hl_file = hl_file->nextfile;
	    }
	    hl_dir = hl_dir->nextdir;
	}
	hl_spool = hl_spool->nextspool;
    }
}



syntax highlighted by Code2HTML, v. 0.9.1