/*
 * Copyright (c) 1998,1999,2000  Kazushi (Jam) Marukawa
 * All rights of my changes are reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice in the documentation and/or other materials provided with
 *    the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
 * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
 * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/* $Orig-Id: texpire.c,v 1.14 1997/07/20 00:37:18 agulbra Exp $

Written by Arnt Gulbrandsen <agulbra@troll.no> and copyright 1995
Troll Tech AS, Postboks 6133 Etterstad, 0602 Oslo, Norway, fax +47
22646949.

Use, modification and distribution is allowed without limitation,
warranty, or liability of any kind.  */

#ifdef SOCKS
#include <socks.h>
#endif

#include <ctype.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <dirent.h>
#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/time.h>
#include <unistd.h>
#include <errno.h>

#include "leafnode.h"

time_t now;

unsigned long default_expire;

struct exp {
    char* xover;
    int kill;
    int exists;
};


/*
05/27/97 - T. Sweeney - Find a group in the expireinfo linked list and return
                        its expire time. Otherwise, return zero.
*/

unsigned long lookup_expire(char* group)
{
    int i;

    for (i = 0; i < groupexpires.num; i++) {
	struct groupexpire* p = &groupexpires.array[i];
        if (myfnmatch(p->group, group) == 0)
            return p->xtime;
    }
    return 0;
}


static void dogroup(struct newsgroup* g)
{
    char gdir[PATH_MAX];
    char* p;
    char* q;
    DIR* d;
    struct dirent* de;
    struct stat st;
    unsigned long first, last, art;
    struct exp* articles;
    int n;
    int fd;
    char* overview; /* xover: read then free */

    int deleted,kept;

    deleted = kept = 0;
    clearidtree();

    /* eliminate empty groups */
    if (!chdirgroup(g, 0))
	return;
    getcwd(gdir, PATH_MAX);

    /* find low-water and high-water marks */

    d = opendir(".");
    if (!d) {
	mysyslog(LOG_ERR, "opendir in %s: %s", gdir, strerror(errno));
	return;
    }

    first = ULONG_MAX;
    last = 0;
    while ((de = readdir(d)) != 0) {
	art = strtoul(de->d_name, &p, 10);
	if (p && *p == '\0') {
	    /*
	     * Need to check stat of each entry because it might be
	     * number named directory like alt.260.
	     *
	     * To reduce the number of stat syscall, do little trick here.
	     */
	    if ((art < first || art > last) &&
		stat(de->d_name, &st) == 0 &&
		(S_ISREG(st.st_mode))) {
		if (art < first)
		    first = art;
		if (art > last)
		    last = art;
	    }
	}
    }
    if (last < first) {
	closedir(d);
	return;
    }
    rewinddir(d);

    /* allocate and clear article array */

    articles = (struct exp*)critmalloc((last - first + 1) * sizeof(struct exp),
				       "Reading articles to expire");
    for (art = 0; art <= last - first; art++) {
	articles[art].xover = NULL;
	articles[art].kill = 0;
	articles[art].exists = 0;
    }

    /* read in overview info, to be purged and written back */

    overview = NULL;

    if (stat(".overview", &st) == 0) {
	/* could use mmap() here but I don't think it would help */
	overview = critmalloc(st.st_size + 1, "Reading article overview info");
	if ((fd = open(".overview", O_RDONLY)) < 0 ||
	    (read(fd, overview, st.st_size) < st.st_size)) {
	    mysyslog(LOG_ERR, "can't open/read %s/.overview: %s",
		     gdir, strerror(errno));
	    *overview = '\0';
	    if (fd > -1)
		close(fd);
	} else {
	    close(fd);
	    overview[st.st_size] = '\0'; /* 0-terminate string */
	}

	p = overview;
	while (p && *p) {
	    while (p && isspace(*p))
		p++;
	    art = strtoul(p, NULL, 10);
	    if (art >= first && art <= last &&
		!articles[art - first].xover) {
		articles[art - first].xover = p;
		articles[art - first].kill = 1;
	    }
	    p = strchr(p, '\n');
	    if (p) {
		*p = '\0';
		if (p[-1] == '\r')
		    p[-1] = '\0';
		p++;
	    }
	}
    }

    /* check the syntax of the .overview info, and delete all illegal stuff */

    for (art = first; art <= last; art++) {
	if (articles[art - first].xover &&
	    !legalxoverline(articles[art - first].xover)) {
	    mysyslog(LOG_INFO, "bad overview line for %s/%lu", g->name, art);
	    articles[art - first].xover = NULL;
	}
    }

    /* insert articles in tree, and clear 'kill' for new or read articles */

    while ((de = readdir(d)) != 0) {
	art = strtoul(de->d_name, &p, 10);
	if (p && *p == '\0' && stat(de->d_name, &st) == 0 &&
	    (S_ISREG(st.st_mode))) {
	    articles[art - first].kill = 1;
	    articles[art - first].exists = 1;
	    if ((expire >  0) && (st.st_mtime > expire)) {
		articles[art - first].kill = 0;
		p = articles[art - first].xover;
		for (n = 0; n < 4; n++)
		    if (p && (p = strchr(p + 1, '\t')))
			p++;
		q = p ? strchr(p, '\t') : NULL;
		if (p && q) {
		    *q = '\0';
		    if (findmsgid(p)) { /* another file with same msgid? */
			articles[art - first].kill = 1;
		    } else {
			insertmsgid(p, art);
			if (st.st_nlink < 2) { /* repair fs damage */
			    if (link(de->d_name, getmsgidfname(p))) {
				if (errno == EEXIST)
				    /* exists, but points to another file */
				    articles[art-first].kill = 1;
				else
				    mysyslog(LOG_ERR, "relink of %s failed: "
					     "%s (%s)", p, strerror(errno),
					     getmsgidfname(p));
			    }
			    else
				mysyslog(LOG_INFO, "relinked message %s", p);
			}
			*q = '\t';
		    }
		} else if (articles[art-first].xover) {
		    /* news database inconsistency: delete and be rid of it */
		    articles[art-first].kill = 1;
		} else {
		    /* possibly read the xover line into memory? */
		}
	    }
	}
    }
    closedir(d);

    /* compute new low-water mark */

    art = first;
    while (art<=last && articles[art-first].kill)
	art++;
    g->first = art;

    /* remove old postings */

    for (art = first; art <= last; art++) {
	char name[20];
	if (articles[art-first].exists) {
	    if (articles[art-first].kill) {
		sprintf(name, "%lu", art);
#if TEST_TEXPIRE
		printf("testing about texpire: will unlink %s/%ld\n",
		       gdir, art);
		deleted++;
#else
		if (!unlink(name)) {
		    deleted++;
		} else if (errno != ENOENT && errno != EEXIST) {
		    kept++;
		    mysyslog(LOG_ERR, "unlink %s/%ld: %s",
			     gdir, art, strerror(errno));
		}
#endif
	    } else {
		kept++;
	    }
	}
    }
    free((char*)articles);
    free(overview);

    if (last > g->last) /* try to correct insane newsgroup info */
	g->last = last;

    if (deleted || kept)
	printf("%s: %d articles deleted, %d kept\n", g->name, deleted, kept);

    if (kept == 0)
	(void)unlink(".overview");

    if (kept == 0 && !chdir("..") && !rmdir(gdir))
	printf("removed unnecessary directory %s\n", gdir);
}


static void expiregroup(struct newsgroup* g)
{
    if (g) {
	expiregroup(g->right);
	expire = lookup_expire(g->name);
	if (!expire) {
	    const char* s = getinterestingngfname(g);
	    struct stat st;
	    expire = default_expire;
	    /* use short expire if the group isn't even possibly interesting */
	    if (stat(s, &st) < 0) {
		expire = time(NULL) - (time_t)(timeout_long * 86400);
	    }
	}
	dogroup(g);
	expiregroup(g->left);
    }
}


static void expiremsgid(void)
{
    int n;
    DIR* d;
    struct dirent* de;
    struct stat st;
    int deleted, kept;

    deleted = kept = 0;

    initconfig();
    for (n = 0; n < 1000; n++) {
	const char* s = getmsgiddname(n);
	if (chdir(s)) {
	    if (errno == ENOENT)
		mkdir(s, 0755); /* file system damage again */
	    if (chdir(s)) {
		mysyslog(LOG_ERR, "chdir %s: %s", s, strerror(errno));
		continue;
	    }
	}

	d = opendir(".");
	if (!d)
	    continue;
	while ((de=readdir(d))) {
	    if (stat(de->d_name, &st) == 0) {
		if (st.st_nlink < 2 && !unlink(de->d_name))
		    deleted++;
		else
		    kept++;
	    }
	}
	closedir(d);
    }

    if (kept || deleted)
	printf("total: %d articles deleted, %d kept\n", deleted, kept);
}


int main(int argc, char** argv)
{
    struct passwd* pw;

    pw = getpwnam("news");
    if (!pw) {
	fprintf(stderr, "no such user: news\n");
	exit(1);
    }

    setgid(pw->pw_gid);
    setreuid(pw->pw_uid, pw->pw_uid);

    if (getuid() != pw->pw_uid || getgid() != pw->pw_gid) {
	fprintf(stderr, "%s: must be run as news or root\n", argv[0]);
	exit(1);
    }

    expire = 0;

    lockactive();
    readactive();
    readconfig();
    default_expire = expire;
    if (expire == 0) {
	fprintf(stderr, "%s: no expire time\n", argv[0]);
	exit(2);
    }

    openlog("expire", LOG_PID|LOG_CONS, LOG_NEWS);

    now = time(NULL);

    expiregroup(active);
    writeactive();
    expiremsgid();
    return 0;
}


syntax highlighted by Code2HTML, v. 0.9.1