/*
 * ratDbase.c --
 *
 * TkRat software and its included text is Copyright 1996-2002 by
 * Martin Forssén
 *
 * The full text of the legal notice is contained in the file called
 * COPYRIGHT, included with this distribution.
 *
 *	This file contains support for a database of messages. This file
 *	uses version 4 of the database. The format of the database is as
 *	follows:
 *
 *	The database directory contains the following entries:
 *	  index		The main database index. This file consists
 *			of a number of entries separated by newline and
 *			each entry has the following format:
 *				To
 *				From
 *				Cc
 *				Message-Id
 *				References
 *				Subject
 *				Date (UNIX time_t as a string)
 *				Keywords (SPACE separated list)
 *				Size
 *				Status
 *				Expiration time (UNIX time_t as a string)
 *				Expiration event (*)
 *				Filename
 *			Expiration event is one of none, remove, incoming,
 *			backup and custom. Custom is followed by the custom
 *			command.
 *	  index.info	This file contains information about the database.
 *			It contains two integers. The first is the version
 *			(in this case 3) and the second is the number of
 *			entries in the indexfile.
 *	  index.changes	This file contains a log of changes made to the
 *			index file. This log is only kept if the multiple
 *			agents has opened the database. In this file each
 *			entry is one line. There are addition entries;
 *			'a OFFSET' where OFFSET is the position in the
 *			index file where this entry starts. There are also
 *			deletion entries; 'd INDEX'. Finally there are
 *			the status changes; they are of the form:
 *			's INDEX STATUS' where STATUS is the new status of
 *			the specified message.
 *	  lock		If this file exists the database is locked and
 *			no other agent may do anything with it. It should
 *			contain a string identifying the agent owning the
 *			lock.
 *	  rlock.*	Each agent that opens the database should construct
 *			a file with the following name: rlock.HOST:PID .
 *			This file should be touched at least once every
 *			hour.
 *	  dbase/	This is a directory which holds a number of
 *			directories which in turn holds the actual messages.
 *
 *	In the dbase directory messages are stored as recipient-name/number.
 *	Where the last number taken in a recipient-name directory is
 *	contained in a .seq file found in said directory.
 *	All entries are stored in utf-8
 */

#include "ratFolder.h"

#define DBASE_VERSION 5		/* Version of the database format */
#define EXTRA_ENTRIES 100	/* How many extra entries we should allocate
				 * room for when allocating the entryPtr
				 * array */
#define RLOCK_TIMEOUT 2*60*60	/* Actual timeout time for rlock files */
#define UPDATE_INTERVAL 40*60	/* Time between updates to the rlock file */

static int isRead = 0;		/* 0 means that the database hasn't been
				 * read yet */
static int numRead;		/* The number of entries in the entryPtr list*/
static int numAlloc;		/* The number of entries that will fit
				 * into the entryPtr list */
static RatDbEntry *entryPtr;	/* The list of entries in the database */
static char *dbDir = 0;		/* Full path to the database directory */
static char *ident = 0; 	/* String which is used to identify us.
				 * It is of the form hostname:pid */
static int changeSize = 0;	/* The number of bytes in the index.changes
				 * file that we have read and incorporated
				 * into our memory resident database */
static int needRewrite = 0;	/* 1 if we need to rewrite the indexfile */
static int numChanges = 0;	/* Number of changes in the changes file */
static int version;		/* Version read */

/*
 * This structure is used while checking the dbase
 */
typedef struct {
    int fileSize;	/* The actual size of the file */
    int index;		/* Index in the list that handles this entry */
    RatDbEntry entry;	/* The actual entry in the index file */
} RatDbItem;

/*
 * Forward declarations for procedures defined in this file:
 */
static int	Read(Tcl_Interp *interp);
static void	Lock(Tcl_Interp *interp);
static void	Unlock(Tcl_Interp *interp);
static int	Sync(Tcl_Interp *interp, int force);
static void	Update(ClientData clientData);
static int	IsRlocked(char *ignore);
static void	RatDbBuildList(Tcl_Interp *interp, Tcl_DString *dsPtr,
	char *prefix, char *dir, Tcl_HashTable *tablePtr, int fix);
static int	NoLFPrint(FILE *fp, const char *s);
static void	DbaseConvert3to4(Tcl_Interp *interp);



/*
 *----------------------------------------------------------------------
 *
 * Read --
 *
 *      Reads the database from disk into memory.
 *
 * Results:
 *      The return value is normally TCL_OK; if something goes wrong
 *	TCL_ERROR is returned and an error message will be left in
 *	the result area.
 *
 * Side effects:
 *      The internal list of entries is allocated and initalized. An
 *	rlock-file is created to indicate that we have the database
 *	open. To keep this lock up to date the Update() procedure must
 *	be called at least once every hour. When the agent won't access
 *	the database anymore it must be closed with a call to RatDbClose().
 *
 *
 *----------------------------------------------------------------------
 */

static int
Read(Tcl_Interp *interp)
{
    char buf[1024];	/* Scratch area */
    int size;		/* Size of indexfile */
    struct stat sbuf;	/* Buffer for stat() calls */
    int fhIndex;	/* File handle for index-file */
    int fhReadlock;	/* File handle for read lock file */
    FILE *fpIndexinfo;	/* File pointer for index.info file */
    int i, j;		/* Loop variables */
    char *cPtr;		/* Running pointer */

    /*
     * First make sure we know where the database should reside and which
     * identifier we should use.
     */
    if (0 == dbDir) {
	const char *value = RatGetPathOption(interp, "dbase_dir");
	if (NULL == value) {
	    return TCL_ERROR;
	}
	dbDir = cpystr(value);
    }
    if (0 == ident) {
	gethostname(buf, sizeof(buf));
	ident = (char*)ckalloc(strlen(buf)+16);
	snprintf(ident, strlen(buf)+16, "%s:%d", buf, (int)getpid());
    }

    /*
     * Check if the database actually exists. If it doesn't then we
     * must create the needed directories and files.
     */
    snprintf(buf, sizeof(buf), "%s/index", dbDir);
    if (       (0 != stat(dbDir, &sbuf) && ENOENT == errno)
	    || (0 != stat(buf, &sbuf) && ENOENT == errno)) {
	if (0 != mkdir(dbDir, DIRMODE) && EEXIST != errno) {
	    Tcl_AppendResult(interp, "error creating directory \"", dbDir,
		    "\": ", Tcl_PosixError(interp), (char *) NULL);
	    return TCL_ERROR;
	}
	snprintf(buf, sizeof(buf), "%s/dbase", dbDir);
	if (0 != mkdir(buf, DIRMODE) && EEXIST != errno) {
	    Tcl_AppendResult(interp, "error creating directory \"", buf,
		    "\": ", Tcl_PosixError(interp), (char *) NULL);
	    return TCL_ERROR;
	}

	Lock(interp);

	snprintf(buf, sizeof(buf), "%s/index", dbDir);
	if (0 > (fhIndex = open(buf, O_CREAT|O_WRONLY, FILEMODE))
		|| 0 != close(fhIndex)) {
	    Unlock(interp);
	    Tcl_AppendResult(interp, "error creating file \"", buf,
		    "\": ", Tcl_PosixError(interp), (char *) NULL);
	    return TCL_ERROR;
	}
	snprintf(buf, sizeof(buf), "%s/index.info", dbDir);
	if (0 == (fpIndexinfo = fopen(buf, "w"))) {
	    Unlock(interp);
	    Tcl_AppendResult(interp, "error creating file \"", buf,
		    "\": ", Tcl_PosixError(interp), (char *) NULL);
	    return TCL_ERROR;
	}
	if (0 > fprintf(fpIndexinfo, "%d 0\n", DBASE_VERSION)) {
	    Unlock(interp);
	    Tcl_AppendResult(interp, "error writing to file \"", buf, "\"",
		    (char *) NULL);
	    return TCL_ERROR;
	}
	if (0 != fclose(fpIndexinfo)) {
	    Unlock(interp);
	    Tcl_AppendResult(interp, "error closing file \"", buf,
		    "\": ", Tcl_PosixError(interp), (char *) NULL);
	    return TCL_ERROR;
	}
    } else {
	Lock(interp);
    }

    /*
     * Create rlock file.
     */
    snprintf(buf, sizeof(buf), "%s/rlock.%s", dbDir, ident);
    if (0 > (fhReadlock = open(buf, O_CREAT|O_WRONLY, FILEMODE))
	    || 0 != close(fhReadlock)) {
	Unlock(interp);
	Tcl_AppendResult(interp, "error creating file \"", buf,
		"\": ", Tcl_PosixError(interp), (char *) NULL);
	return TCL_ERROR;
    }
    (void)Tcl_CreateTimerHandler(UPDATE_INTERVAL*1000,Update,(ClientData)NULL);

    /*
     * Read the index.info file
     */
    snprintf(buf, sizeof(buf), "%s/index.info", dbDir);
    if (0 == (fpIndexinfo = fopen(buf, "r"))) {
	Tcl_AppendResult(interp, "error opening file (for reading)\"", buf,
		"\": ", Tcl_PosixError(interp), (char *) NULL);
	goto error;
    }
    if ( 2 != fscanf(fpIndexinfo, "%d %d", &version, &numRead)) {
	Tcl_SetResult(interp, "index.info file corrupt", TCL_STATIC);
	fclose(fpIndexinfo);
	goto error;
    }
    fclose(fpIndexinfo);

    /*
     * Check if this is the current version of the database. If not
     * complain!
     */
    if (version != DBASE_VERSION && version != 3) {
	snprintf(buf, sizeof(buf),
		 "wrong version of database got %d expected %d", version,
		 DBASE_VERSION);
	Tcl_SetResult(interp, buf, TCL_VOLATILE);
	goto error;
    }

    /*
     * Read the indexfile and build internal data structures
     */
    snprintf(buf, sizeof(buf), "%s/index", dbDir);
    if (0 != stat(buf, &sbuf)) {
	Tcl_AppendResult(interp, "error stating file \"", buf,
		"\": ", Tcl_PosixError(interp), (char *) NULL);
	goto error;
    }
    size = sbuf.st_size;
    if (size > 0) {
	char *indexPtr;	/* Pointer to read version of the indexfile */

	/*
	 * We should not free this value since we never close the database.
	 * Purify will note it as a leak, but we will have lots of
	 * pointers into this data.
	 */
	if ( 0 == (indexPtr = (char*)ckalloc(size))) {
	    Tcl_SetResult(interp, "failed to allocate memory for index",
		    TCL_STATIC);
	    goto error;
	}
	fhIndex = open(buf, O_RDONLY);
	if (size != read(fhIndex, indexPtr, size)) {
	    Tcl_SetResult(interp, "error reading index", TCL_STATIC);
	    close(fhIndex);
	    goto error;
	}
	close(fhIndex);
	entryPtr = (RatDbEntry*)ckalloc((numRead+EXTRA_ENTRIES) *
		sizeof(RatDbEntry));
	numAlloc = numRead+EXTRA_ENTRIES;

	cPtr = indexPtr;
	for (i=0; i<numRead; i++) {
	    for (j=0; j<RATDBETYPE_END; j++) {
		entryPtr[i].content[j] = cPtr;
		while (cPtr <= &indexPtr[size] && *cPtr != '\n') {
		    cPtr++;
		}
		if (cPtr > &indexPtr[size]) {
		    Tcl_SetResult(interp, "error in index-file", TCL_STATIC);
		    ckfree(indexPtr);
		    goto error;
		}
		*cPtr++ = '\0';
	    }
	    /*
	     * This is a KLUDGE to work around a bug which existed for a
	     * short time /MaF 960218
	     */
	    if ('+' == entryPtr[i].content[EX_TYPE][0]) {
		char buf[64];
		sprintf(buf, "%ld",
			atoi(entryPtr[i].content[EX_TYPE])*24*60*60+
			atol(entryPtr[i].content[DATE]));
		entryPtr[i].content[EX_TIME] = cpystr(buf);
		entryPtr[i].content[EX_TYPE] = "backup";
	    }
	}
    } else {
	entryPtr = (RatDbEntry*)ckalloc(EXTRA_ENTRIES * sizeof(RatDbEntry));
	numAlloc = EXTRA_ENTRIES;
    }
    isRead = 1;

    /*
     * Let's get up to date with any changes made.
     */
    if (3 == version) {
	Sync(interp, 1);
	DbaseConvert3to4(interp);
	Unlock(interp);
	return Read(interp);
    } else {
	Sync(interp, 0);
    }
    Unlock(interp);

    return TCL_OK;

error:
    snprintf(buf, sizeof(buf), "%s/rlock.%s", dbDir, ident);
    unlink(buf);
    Unlock(interp);
    return TCL_ERROR;
}


/*
 *----------------------------------------------------------------------
 *
 * Lock --
 *
 *      Exculsively lock the database. A lock like this must be obtained
 *	before you do anything at all with the database. And while a
 *	lock like this is active nobody but the owner of the lock may
 *	do anything to the database. The lock obtained must be released
 *	with a call to the Unlock() procedure.
 *
 * Results:
 *      None.
 *
 * Side effects:
 *      Creates a lockfile in the database directory.
 *
 *----------------------------------------------------------------------
 */

static void
Lock(Tcl_Interp *interp)
{
    char buf[1024];	/* Scratch area */
    int fhLock;		/* Lockfile filehandle */
    int msgPost = 0;	/* True if message has been posted */

    do {
	snprintf(buf, sizeof(buf), "%s/lock", dbDir);
	if (-1 == (fhLock = open(buf, O_CREAT|O_EXCL|O_WRONLY, FILEMODE))) {
	    if (EEXIST == errno) {
		if (!msgPost) {
		    RatLogF(interp, RAT_INFO, "waiting_dbase_lock",
			    RATLOG_EXPLICIT);
		    Tcl_Eval(interp, buf);
		    msgPost = 1;
		}
		sleep(2);
	    } else {
		RatLogF(interp, RAT_FATAL, "failed_to_create_file",
			RATLOG_TIME, buf, Tcl_PosixError(interp));
		exit(1);
	    }
	}
    } while (-1 == fhLock);
    write(fhLock, ident, strlen(ident));
    close(fhLock);
    if (msgPost) {
	RatLog(interp, RAT_INFO, "", RATLOG_TIME);
    }
}


/*
 *----------------------------------------------------------------------
 *
 * Unlock --
 *
 *      Releases a lock previously obtained with the Lock() procedure.
 *
 * Results:
 *      None.
 *
 * Side effects:
 *      The lockfile is removed.
 *
 *----------------------------------------------------------------------
 */

static void
Unlock(Tcl_Interp *interp)
{
    char buf[1024];	/* Scratch area */

    snprintf(buf, sizeof(buf), "%s/lock", dbDir);
    if (0 != unlink(buf)) {
	RatLogF(interp, RAT_FATAL, "failed_to_unlink_file", RATLOG_TIME, buf,
		Tcl_PosixError(interp));
	exit(1);
    }
}


/*
 *----------------------------------------------------------------------
 *
 * Sync --
 *
 *      Make sure that the database in memory is consistent with the
 *	master on disk. This is accomplished by reading the index.changes
 *	file. This call assumes that we have an exclusive lock on the
 *	database.
 *
 * Results:
 *      The return value is normally TCL_OK; if something goes wrong
 *	TCL_ERROR is returned and an error message will be left in
 *	the result area.
 *
 * Side effects:
 *      The internal list of entries may be affected as well as the
 *	index-file on disk.
 *
 *----------------------------------------------------------------------
 */

static int
Sync(Tcl_Interp *interp, int force)
{
    char buf[1024];	   /* Scratch area */
    struct stat sbuf;	   /* Buffer for stat() calls */
    FILE *fpChanges;	   /* index.changes file pointer */
    FILE *fpIndex;	   /* Index file pointer */
    char command;	   /* Command in changes file */
    int cmdArg;		   /* Argument to command */
    int i;		   /* Loop counter */
    int doWrite = 0;	   /* 1 if we should write the changes to the disk */
    int numEntries;	   /* How many entries there actually are */
    char *indexBuf = NULL; /* New part of index file */
    int indexOffset = 0;   /* Offset of new part of index file */
    char *cPtr;
    int size;

    snprintf(buf, sizeof(buf), "%s/index.changes", dbDir);
    if (0 > stat(buf, &sbuf)) {
	if (ENOENT != errno) {
	    Tcl_AppendResult(interp, "error stating file \"", buf,
		    "\": ", Tcl_PosixError(interp), (char *) NULL);
	    return TCL_ERROR;
	}
	if (!force) {
	    return TCL_OK;
	} else {
	    sbuf.st_size = 0;
	}
    }
    if (changeSize >= sbuf.st_size && !force) {
	return TCL_OK;
    }

    /*
     * Read and perform changes mentioned in index.changes file
     */
    if (0 == (fpChanges = fopen(buf, "r"))) {
	Tcl_AppendResult(interp, "error opening file (for reading) \"",
		buf, "\": ", Tcl_PosixError(interp), (char *) NULL);
	return TCL_ERROR;
    }
    if (0 != fseek(fpChanges, changeSize, SEEK_SET)) {
	Tcl_AppendResult(interp, "error seeking in file \"", buf,
		"\": ", Tcl_PosixError(interp), (char *) NULL);
	fclose(fpChanges);
	return TCL_ERROR;
    }
    changeSize = sbuf.st_size;
    while(1) {
	if (2 != fscanf(fpChanges, "%c %d ", &command, &cmdArg) ||
		('d'!=command && 'a'!=command && 's'!=command)
		|| ('s' == command &&
		buf != fgets(buf, sizeof(buf), fpChanges))) {
	    if (feof(fpChanges)) {
		break;
	    }
	    Tcl_SetResult(interp, "syntax error in changes file",
		    TCL_STATIC);
	    fclose(fpChanges);
	    return TCL_ERROR;
	}
	numChanges++;
	if ('d' == command) {
	    if (cmdArg < 0 || cmdArg >= numRead) {
		continue;
	    }
	    needRewrite = 1;
	    entryPtr[cmdArg].content[FROM] = NULL;
	} else if ('s' == command) {
	    if (cmdArg < 0 || cmdArg >= numRead) {
		continue;
	    }
	    needRewrite = 1;
	    buf[strlen(buf)-1] = '\0';
	    if ( (int) strlen(buf) <=
		    (int) strlen(entryPtr[cmdArg].content[STATUS])){
		strcpy(entryPtr[cmdArg].content[STATUS], buf);
	    } else {
		/*
		 * This code may leak the memory occupied by the
		 * previous status string. I believe this loss can
		 * be lived with (it should be quite rare).
		 */
		entryPtr[cmdArg].content[STATUS] =
			(char *) ckalloc(strlen(buf)+1);
		strcpy(entryPtr[cmdArg].content[STATUS], buf);
	    }
	} else {
	    if (numRead == numAlloc) {
		numAlloc += EXTRA_ENTRIES;
		entryPtr = (RatDbEntry*)ckrealloc(entryPtr,
			numAlloc*sizeof(RatDbEntry));
	    }
	    if (!indexBuf) {
		snprintf(buf, sizeof(buf), "%s/index", dbDir);
		if (NULL == (fpIndex = fopen(buf, "r"))) {
		    Tcl_AppendResult(interp,
			    "error opening file (for reading) \"", buf,
			    "\": ", Tcl_PosixError(interp), (char *) NULL);
		    fclose(fpChanges);
		    return TCL_ERROR;
		}
		if (0 != fseek(fpIndex, cmdArg, SEEK_SET)) {
		    Tcl_AppendResult(interp, "error seeking in file \"",buf,
			    "\": ", Tcl_PosixError(interp), (char *) NULL);
		    fclose(fpIndex);
		    fclose(fpChanges);
		    return TCL_ERROR;
		}
		(void)fstat(fileno(fpIndex), &sbuf);
		/*
		 * Purify will probably report this as a leak, but that
		 * is not true.
		 */
		size = sbuf.st_size - cmdArg;
		indexBuf = (char*)ckalloc(size + 1);
		fread(indexBuf, size, 1, fpIndex);
		fclose(fpIndex);
		indexBuf[size] = '\0';
		indexOffset = cmdArg;
	    }
	    cPtr = indexBuf + (cmdArg - indexOffset);
	    for (i=0; i<RATDBETYPE_END; i++) {
		entryPtr[numRead].content[i] = cPtr;
		for (; *cPtr != '\n' && *cPtr; cPtr++);
		if (!*cPtr) {
		    Tcl_AppendResult(interp, "error reading \"",buf,
			    "\": ", Tcl_PosixError(interp),(char*)NULL);
		    fclose(fpChanges);
		    return TCL_ERROR;
		}
		*cPtr++ = '\0';
	    }
	    numRead++;
	}
    }
    fclose(fpChanges);

    /* 
     * If the number of changes is at least 20 and we are the only agent
     * which have the database open we write the changes into the main
     * index. This also happens if we have read an older version of the
     * database.
     */
    if (20 <= numChanges || force) {
	char myLock[1024];		/* Name of my rlock file */
	snprintf(myLock, sizeof(myLock), "rlock.%s", ident);

	if (IsRlocked(myLock) && !force) {
	    doWrite = 0;
	} else {
	    doWrite = 1;
	}
    }

    if (doWrite) {
	FILE *fpIndexinfo;		/* Filepointer to index info */
	if (needRewrite || force) {
	    char oldIndex[1024];	/* Name of old index file */
	    char newIndex[1024];	/* Name of new index file */
	    FILE *fpNewIndex;		/* Filepointer to new index */
	    int j;			/* Loop variable */

	    snprintf(oldIndex, sizeof(oldIndex), "%s/index", dbDir);
	    snprintf(newIndex, sizeof(newIndex), "%s/index.new", dbDir);
	    if (0 == (fpNewIndex = fopen(newIndex, "w"))) {
		Tcl_AppendResult(interp, "error creating file \"", newIndex,
			"\": ", Tcl_PosixError(interp), (char *) NULL);
		return TCL_ERROR;
	    }
	    if (0 == (fpIndex = fopen(oldIndex, "r"))) {
		Tcl_AppendResult(interp, "error opening file (for reading)\"",
			oldIndex,"\": ", Tcl_PosixError(interp), (char*)NULL);
		(void)fclose(fpNewIndex);
		return TCL_ERROR;
	    }

	    for (i=0, numEntries=0 ; i < numRead; i++) {
		if (0 != entryPtr[i].content[FROM]) {
		    numEntries++;
		    for (j=0; j<RATDBETYPE_END; j++) {
			if (0 > fprintf(fpNewIndex, "%s\n",
				entryPtr[i].content[j])) {
			    Tcl_AppendResult(interp,"error writing to file \"",
				    newIndex, "\"", (char *) NULL);
			    (void)fclose(fpNewIndex);
			    (void)unlink(newIndex);
			    (void)fclose(fpIndex);
			    return TCL_ERROR;
			}
		    }
		} else {
		    snprintf(buf, sizeof(buf), "%s/dbase/%s", dbDir,
			    entryPtr[i].content[FILENAME]);
		    (void)unlink(buf);
		}
	    }
	    (void)fclose(fpIndex);
	    if (0 != fclose(fpNewIndex)) {
		Tcl_AppendResult(interp,"error closing file \"", newIndex,
			"\": ", Tcl_PosixError(interp), (char *) NULL);
		(void)unlink(newIndex);
		return TCL_ERROR;
	    }
	    if (0 != rename(newIndex, oldIndex)) {
		Tcl_AppendResult(interp,"error moving file \"", newIndex,
			"\" -> \"", oldIndex, "\": ", Tcl_PosixError(interp),
			(char *) NULL);
		return TCL_ERROR;
	    }
	} else {
	    numEntries = numRead;
	}
	snprintf(buf, sizeof(buf), "%s/index.info", dbDir);
	if (0 == (fpIndexinfo = fopen(buf, "w"))) {
	    Tcl_AppendResult(interp, "error opening file (for writing)\"",
		    buf, "\": ", Tcl_PosixError(interp), (char *) NULL);
	    return TCL_ERROR;
	}
	if (0 > fprintf(fpIndexinfo, "%d %d\n", DBASE_VERSION, numEntries)) {
	    Tcl_AppendResult(interp, "error writing to file \"", buf, "\"",
		    (char *) NULL);
	    return TCL_ERROR;
	}
	if (0 > fclose(fpIndexinfo)) {
	    Tcl_AppendResult(interp, "error closing file \"", buf,
		    "\": ", Tcl_PosixError(interp), (char *) NULL);
	    return TCL_ERROR;
	}
	snprintf(buf, sizeof(buf), "%s/index.changes", dbDir);
	if (0 != unlink(buf)) {
	    Tcl_AppendResult(interp, "error unlinking file \"", buf,
		    "\": ", Tcl_PosixError(interp), (char *) NULL);
	    return TCL_ERROR;
	}
	changeSize = 0;
	numChanges = 0;
	needRewrite = 0;
	version = DBASE_VERSION;
    }

    return TCL_OK;
}


/*
 *----------------------------------------------------------------------
 *
 * Update --
 *
 *      This routine updates the read-lock we have on the database. This
 *	must be done at least as often as is specified by the Read
 *	rotine. Preferably more often. If the database hasn't been read
 *	yet (or has been closed) this routine does nothing.
 *
 * Results:
 *      None.
 *
 * Side effects:
 *      The rlock-file will be touched (if the database is open).
 *
 *----------------------------------------------------------------------
 */

static void
Update(ClientData clientData)
{
    char rlockName[1024];	/* Name of rlock file */

    if (0 != isRead) {
	return;
    }

    snprintf(rlockName, sizeof(rlockName), "%s/rlock.%s", dbDir, ident);
    (void)utime(rlockName, (struct utimbuf*) NULL);

    (void)Tcl_CreateTimerHandler(UPDATE_INTERVAL*1000,Update,(ClientData)NULL);
}

/*
 *----------------------------------------------------------------------
 *
 * IsRlocked --
 *
 *      Checks if any othe rprocess has an read lock on the database.
 *
 * Results:
 *      True if any othe rprocess has.
 *
 * Side effects:
 *      Stale rlock-files will be removed
 *
 *----------------------------------------------------------------------
 */

static int
IsRlocked(char *ignore)
{
    time_t deadline;		/* Lockfiles older than this can be ignored */
    struct dirent *direntPtr;
    struct stat sbuf;
    int result = 0;
    DIR *dirPtr;
    char buf[1024];

    deadline = time((time_t*) NULL) - RLOCK_TIMEOUT;

    dirPtr = opendir(dbDir);
    while (0 != (direntPtr = readdir(dirPtr))) {
	if (!strncmp("rlock.", (char*)direntPtr->d_name, 6)
		&& (ignore && strcmp((char*)direntPtr->d_name, ignore))) {
	    snprintf(buf, sizeof(buf),"%s/%s", dbDir,(char*)direntPtr->d_name);
	    (void)stat(buf, &sbuf);
	    if (deadline < sbuf.st_mtime) {
		result = 1;
		break;
	    } else {
		unlink(buf);
	    }
	}
    }
    closedir(dirPtr);
    return result;
}


/*
 *----------------------------------------------------------------------
 *
 * RatDbInsert --
 *
 *      This procedure inserts a copy of the message, whose id is passed
 *	in the mail parameter, into the database. An entry is made in the
 *	index file. One of the arguments is the expiration date as a string.
 *	This string is the number of days the message should stay in the
 *	database until it expires.
 *
 *	The algorithm is to update the index on disk and then let Sync()
 *	insert the new value into the internal database.
 *
 * Results:
 *      The return value is normally TCL_OK; if something goes wrong
 *	TCL_ERROR is returned and an error message will be left in
 *	the result area.
 *
 * Side effects:
 *      The internal and external databases are updated.
 *
 *----------------------------------------------------------------------
 */

int
RatDbInsert (Tcl_Interp *interp, const char *to, const char *from,
	     const char *cc, const char *msgid, const char *ref,
	     const char *subject, long date, const char *flags,
	     const char *keywords, long exDate, const char *exType,
	     const char *fromline, const char *mail, int length)
{
    static char *tobuf = NULL;	/* Scratch area */
    static int tobufsize = 0;	/* Size of scratch area */
    char fname[1024];		/* filename of new entry */
    char buf[1024];		/* Scratch area */
    char *dir = NULL;		/* Message directory */
    FILE *indexFP;		/* File pointer to index file */
    long indexPos;		/* Start position in index file */
    char *cPtr;			/* Misc character pointer */
    FILE *seqFP;		/* Filepointer to seq file */
    int seq;			/* sequence number */
    FILE *indchaFP;		/* File pointer to the index.changes file */
    Tcl_Channel dataChannel;	/* Data channel */
    ADDRESS *adrPtr;		/* Address list */
    int mode;			/* Mode to use when creating files */
    int i;
    Tcl_Obj *oPtr;

    /*
     * Get file creation mode
     */
    oPtr = Tcl_GetVar2Ex(interp, "option", "permissions", TCL_GLOBAL_ONLY);
    Tcl_GetIntFromObj(interp, oPtr, &mode);

    if (0 == isRead) {
	if (TCL_OK != Read(interp)) {
	    return TCL_ERROR;
	}
    }
    Lock(interp);

    /*
     * Generate the filename we are to use for this entry in the database.
     * Create the directory it will be stored in too (if needed).
     */
    adrPtr = NULL;
    if (to && *to) {
	if (tobufsize < strlen(to)+1) {
	    tobufsize = strlen(to)+1;
	    tobuf = (char*)ckrealloc(tobuf, tobufsize);
	}
	strlcpy(tobuf, to, tobufsize);
	rfc822_parse_adrlist(&adrPtr, tobuf, "not.used");
	if (adrPtr && adrPtr->mailbox && *adrPtr->mailbox) {
	    dir = cpystr(adrPtr->mailbox);
	}
    }
    if (!dir) {
	dir = cpystr("default");
    }
    mail_free_address(&adrPtr);
    for (cPtr = dir; *cPtr; cPtr++) {
	if ('/' == *cPtr) {
	    *cPtr = '_';
	}
    }

    snprintf(fname, sizeof(fname), "%s/", dir);
    snprintf(buf, sizeof(buf), "%s/dbase/%s/.seq", dbDir, dir);
    if (NULL == (seqFP = fopen(buf, "r+"))) {
	snprintf(buf, sizeof(buf), "%s/dbase/%s", dbDir, dir);
	if (0 != mkdir(buf, DIRMODE) && EEXIST != errno) {
	    Unlock(interp);
	    Tcl_AppendResult(interp, "error creating directory \"", buf,
		    "\": ", Tcl_PosixError(interp), (char *) NULL);
	    ckfree(dir);
	    return TCL_ERROR;
	}
	seq = 0;
	snprintf(buf, sizeof(buf), "%s/dbase/%s/.seq", dbDir, dir);
	if (NULL == (seqFP = fopen(buf, "w"))) {
	    Unlock(interp);
	    Tcl_AppendResult(interp, "error opening (for writing)\"", buf,
		    "\": ", Tcl_PosixError(interp), (char *) NULL);
	    ckfree(dir);
	    return TCL_ERROR;
	}
    } else {
	if (1 != fscanf(seqFP, "%d", &seq)) {
	    (void)fclose(seqFP);
	    Unlock(interp);
	    Tcl_AppendResult(interp, "error parsing: \"", buf, "\"",
		    (char*) NULL);
	    ckfree(dir);
	    return TCL_ERROR;
	}
	seq++;
    }
    ckfree(dir);
    rewind(seqFP);
    if (0 > fprintf(seqFP, "%d", seq)) {
	(void)fclose(seqFP);
	Unlock(interp);
	Tcl_AppendResult(interp, "error writing to \"", buf,
		    "\": ", Tcl_PosixError(interp), (char *) NULL);
	return TCL_ERROR;
    }
    if (0 != fclose(seqFP)) {
	Unlock(interp);
	Tcl_AppendResult(interp, "error closing file \"", buf,
		"\": ", Tcl_PosixError(interp), (char *) NULL);
	return TCL_ERROR;
    }
    sprintf(buf, "%d", seq);
    cPtr = fname + strlen(fname);
    for (i=strlen(buf)-1; i>=0; i--) {
	*cPtr++ = buf[i];
    }
    *cPtr = '\0';

    /*
     * Open the indexfile and remember where we are
     */
    snprintf(buf, sizeof(buf), "%s/index", dbDir);
    if (NULL == (indexFP = fopen(buf, "a"))) {
	Unlock(interp);
	Tcl_AppendResult(interp, "error opening (for append)\"", buf,
		"\": ", Tcl_PosixError(interp), (char *) NULL);
	return TCL_ERROR;
    }
    indexPos = ftell(indexFP);

    /*
     * Construct the entries... some are simple some others are more
     * complicated :-) The order and format of the entries is documented
     * at the top of this file.
     */
    NoLFPrint(indexFP, to);
    NoLFPrint(indexFP, from);
    NoLFPrint(indexFP, cc);
    NoLFPrint(indexFP, msgid);
    NoLFPrint(indexFP, ref);
    NoLFPrint(indexFP, subject);
    fprintf(indexFP, "%ld\n", date);
    NoLFPrint(indexFP, (keywords ? keywords : ""));
    fprintf(indexFP, "%d\n", length);
    NoLFPrint(indexFP, flags);
    fprintf(indexFP, "%ld\n", exDate*24*60*60 + time((time_t*) NULL));
    NoLFPrint(indexFP, exType);
    if (0 > NoLFPrint(indexFP, fname)) {
	goto losing;
    }
    if (0 != fclose(indexFP)) {
	Tcl_AppendResult(interp, "error closing index file :",
		Tcl_PosixError(interp), (char *) NULL);
	goto losing;
    }

    /*
     * Create the actual entry in the database.
     */
    snprintf(buf, sizeof(buf), "%s/dbase/%s", dbDir, fname);
    if (NULL == (dataChannel = Tcl_OpenFileChannel(interp, buf, "w", mode))) {
	Tcl_AppendResult(interp, "error creating file \"", buf,
		"\": ", Tcl_PosixError(interp), (char *) NULL);
	goto losing;
    }
    /* The casts here are needed to build on tcl <8.4 */
    Tcl_Write(dataChannel, (char*)fromline, strlen(fromline));
    RatTranslateWrite(dataChannel, (char*)mail, length);
    if (TCL_OK != Tcl_Close(interp, dataChannel)) {
	Tcl_AppendResult(interp, "error closing file \"", buf,
		"\": ", Tcl_PosixError(interp), (char *) NULL);
	goto losing_but_nearly_got_it;
    }

    /*
     * Write an entry to the index.changes file and then update
     */
    snprintf(buf, sizeof(buf), "%s/index.changes", dbDir);
    if (NULL == (indchaFP = fopen(buf, "a"))) {
	Tcl_AppendResult(interp, "error opening file (for append)\"", buf,
		"\": ", Tcl_PosixError(interp), (char *) NULL);
	goto losing_but_nearly_got_it;
    }
    if (0 > fprintf(indchaFP, "a %ld\n", indexPos)) {
	Tcl_AppendResult(interp, "error writing to file \"", buf, "\"",
		(char *) NULL);
	goto losing_but_nearly_got_it;
    }
    if (0 != fclose(indchaFP)) {
	Tcl_AppendResult(interp, "error closing file \"", buf,
		"\": ", Tcl_PosixError(interp), (char *) NULL);
	goto losing_but_nearly_got_it;
    }
    Sync(interp, 0);

    Unlock(interp);
    return TCL_OK;

losing_but_nearly_got_it:
    snprintf(buf, sizeof(buf), "%s/dbase/%s", dbDir, fname);
    (void)unlink(buf);

losing:
    (void)snprintf(buf, sizeof(buf), "%s/index", dbDir);
    (void)truncate(buf, indexPos);
    Unlock(interp);

    return TCL_ERROR;
}


/*
 *----------------------------------------------------------------------
 *
 * RatDbSetStatus --
 *
 *	This procedure modifies the status of the given message.
 *
 * Results:
 *      The return value is normally TCL_OK; if something goes wrong
 *	TCL_ERROR is returned and an error message will be left in
 *	the result area.
 *
 * Side effects:
 *      The internal and external databases are updated.
 *
 *----------------------------------------------------------------------
 */

int
RatDbSetStatus(Tcl_Interp *interp, int index, char *status)
{
    char buf[1024];	/* Name of index.changes file */
    FILE *indexFP;	/* FIle pointer to index.changes file */

    /*
     * Check the index for validity.
     */
    if (index >= numRead || index < 0) {
	Tcl_SetResult(interp, "error: the given index is invalid", TCL_STATIC);
	return TCL_ERROR;
    }

    /*
     * Check if we really need to do this
     */
    if (!strcmp(status, entryPtr[index].content[STATUS])) {
	return TCL_OK;
    }

    Lock(interp);
    snprintf(buf, sizeof(buf), "%s/index.changes", dbDir);
    if (NULL == (indexFP = fopen(buf, "a"))) {
	Tcl_AppendResult(interp, "error opening (for append)\"", buf,
		"\": ", Tcl_PosixError(interp), (char *) NULL);
	Unlock(interp);
	return TCL_ERROR;
    }
    if (0 > fprintf(indexFP, "s %d %s\n", index, status)){
	Tcl_AppendResult(interp, "Failed to write to file \"", buf, "\"",
		(char*) NULL);
	(void)fclose(indexFP);
	Unlock(interp);
	return TCL_ERROR;
    }
    if (0 != fclose(indexFP)) {
	Tcl_AppendResult(interp, "error closing file \"", buf,
		"\": ", Tcl_PosixError(interp), (char *) NULL);
	Unlock(interp);
	return TCL_ERROR;
    }

    Sync(interp, 0);
    Unlock(interp);
    return TCL_OK;
}


/*
 *----------------------------------------------------------------------
 *
 * RatDbSearch --
 *
 *      Searches the database for entries matching the given expression.
 *	The search expression is in the following form:
 *	    op exp [exp ...]
 *	Where op is either "and" or "or". and exp is as follows:
 *	    [not] field value
 *	Where field is one of "to", "from", "cc", "subject", "keywords"
 *	and "all". if op is "and" the all following expressions must be
 *	true but if it is "or" then only one has to be true.
 *
 * Results:
 *      The number of items found is returned in numFoundPtr if it is
 *	non zero a pointer to a list of found ones is put into *foundPtrPtr.
 *	It is the callers resopnsibility to free this list with a
 *	call to free(). The rotine normally returns TCL_OK except when
 *	there is an error. In that case TCL_ERROR is returned and the cause
 *	may be found in interp->error.
 *
 * Side effects:
 *      None.
 *
 *----------------------------------------------------------------------
 */

int
RatDbSearch(Tcl_Interp *interp, Tcl_Obj *exp, int *numFoundPtr,
	    int **foundPtrPtr)
{
    int or;			/* Indicates the operation; 0 = and  1 = or */
    int i, j, k;		/* Loop counters */
    int match;
    int numAlloc = 0;		/* Number of entries allocated room for */
    int expNumWords, objc;
    Tcl_Obj **expWords, **objv;
    int numExp;			/* The number of subexpressions in the exp */
    char fname[1024];		/* Filename of actual message */
    int bodyfd;			/* File descriptor to actual message */
    char *message = NULL;	/* Actual message */
    int messageSize = 0;	/* Size of message area */
    struct stat sbuf;		/* Buffer for stat calls */
    char *s;
    
    /*
     * The following three lists describes the search expression. Every
     * expression has one entry in each list.
     */
    int *notPtr;
    RatDbEType *fieldPtr;
    Tcl_Obj **valuePtr;

    *numFoundPtr = 0;
    *foundPtrPtr = NULL;

    /*
     * Parse the expression and build the lists.
     */
    if (TCL_OK != Tcl_ListObjGetElements(interp, exp,&expNumWords,&expWords)) {
	return TCL_ERROR;
    }
    s = Tcl_GetString(expWords[0]);
    if (!strcmp(s, "and") && !strcmp(s, "or")) {
	Tcl_SetResult(interp, "exp must start with \"and\" or \"or\".",
		TCL_STATIC);
	return TCL_ERROR;
    }
    notPtr = (int*) ckalloc(sizeof(int) * (expNumWords/2));
    fieldPtr = (RatDbEType*) ckalloc(sizeof(RatDbEType) * (expNumWords/2));
    valuePtr = (Tcl_Obj**) ckalloc(sizeof(Tcl_Obj*) * (expNumWords/2));

    expNumWords--;
    if (!strcmp(s, "or")) {
	or = 1;
    } else {
	or = 0;
    }

    numExp = 0;
    i = 1;
    while (i < expNumWords) {
	s = Tcl_GetString(expWords[i]);
	if (!strcmp(s, "not")) {
	    notPtr[numExp] = 1;
	    i++;
	    s = Tcl_GetString(expWords[i]);
	} else {
	    notPtr[numExp] = 0;
	}

	if (i > expNumWords-1) {
	    Tcl_SetResult(interp, "Parse error in exp (to few words)",
		    TCL_STATIC);
	    goto losing;
	}
	if (!strcmp(s, "to")) {
	    fieldPtr[numExp] = TO;
	} else if (!strcmp(s, "from")) {
	    fieldPtr[numExp] = FROM;
	} else if (!strcmp(s, "cc")) {
	    fieldPtr[numExp] = CC;
	} else if (!strcmp(s, "subject")) {
	    fieldPtr[numExp] = SUBJECT;
	} else if (!strcmp(s, "keywords")) {
	    fieldPtr[numExp] = KEYWORDS;
	} else if (!strcmp(s, "all")) {
	    fieldPtr[numExp] = -1;
	} else {
	    Tcl_SetResult(interp, "Parse error in exp (illegal field value)",
		    TCL_STATIC);
	    goto losing;
	}
	i++;
	valuePtr[numExp++] = expWords[i++];
    }

    /*
     * Now we are ready to do the searching. First make sure that the
     * database is read and synced, then run through it.
     */
    if (0 == isRead) {
	if (TCL_OK != Read(interp)) {
	    goto losing;
	}
    } else {
	if (TCL_OK != Sync(interp, 0)) {
	    goto losing;
	}
    }
    for (i=0; i < numRead; i++) {
	if (!entryPtr[i].content[FROM]) {	/* Entry deleted */
	    continue;
	}
	match = 0;
	for(j=0; j < numExp && !(j != 0 && or == match); j++) {
	    Tcl_ListObjGetElements(interp, valuePtr[j], &objc, &objv);
	    for (k=0; k<objc && !(k != 0 && or == match); k++) {
		if (fieldPtr[j] == -1) {
		    snprintf(fname, sizeof(fname), "%s/dbase/%s", dbDir,
			    entryPtr[i].content[FILENAME]);
		    if (0 > (bodyfd = open(fname, O_RDONLY))) {
			Tcl_AppendResult(interp,
				"error opening file (for read)\"", fname,
				"\": ", Tcl_PosixError(interp), (char*)NULL);
			goto losing;
		    }
		    if (0 != fstat(bodyfd, &sbuf)) {
			Tcl_AppendResult(interp, "error stating file \"",fname,
				"\": ", Tcl_PosixError(interp), (char *) NULL);
			close(bodyfd);
			goto losing;
		    }
		    if (messageSize < sbuf.st_size+1) {
			ckfree(message);
			message = (char*)ckalloc(messageSize = sbuf.st_size+1);
		    }
		    read(bodyfd, message, sbuf.st_size);
		    message[sbuf.st_size] = '\0';
		    (void)close(bodyfd);
		    match = RatSearch(Tcl_GetString(objv[k]), message);
		} else {
		    match = RatSearch(Tcl_GetString(objv[k]),
				      entryPtr[i].content[fieldPtr[j]]);
		}
		if (1 == notPtr[j]) {
		    match = match ? 0 : 1;
		}
	    }
	}

	if (match) {
	    if (*numFoundPtr >= numAlloc) {
		numAlloc += EXTRA_ENTRIES;
		*foundPtrPtr =(int*)ckrealloc(*foundPtrPtr,
					      numAlloc*sizeof(int));
	    }
	    (*foundPtrPtr)[(*numFoundPtr)++] = i;
	}
    }

    ckfree((char*) notPtr);
    ckfree((char*) fieldPtr);
    ckfree((char*) valuePtr);
    if (messageSize > 0) {
	ckfree(message);
    }

    return TCL_OK;

losing:
    ckfree((char*) expWords);
    ckfree((char*) notPtr);
    ckfree((char*) fieldPtr);
    ckfree((char*) valuePtr);
    if (messageSize > 0) {
	ckfree(message);
    }

    return TCL_ERROR;
}


/*
 *----------------------------------------------------------------------
 *
 * RatDbGetEntry --
 *
 *      This routine retrieves an entry from the database. The pointer
 *	returned is ONLY good until the next call to RatDbInsert(),
 *	RatDbSetStatus(), RatDbSearch(), RatDbDelete().
 *
 * Results:
 *      The routine returns a pointer to a RatDbEntry structure which
 *	should be treated as read only. If the index is invalid or
 *	points to a deleted entry a null pointer is returned.
 *
 * Side effects:
 *      None.
 *
 *----------------------------------------------------------------------
 */

RatDbEntry*
RatDbGetEntry(int index)
{
    if (index<0 || index>=numRead || NULL == entryPtr[index].content[FROM]) {
	return NULL;
    }

    return &entryPtr[index];
}


/*
 *----------------------------------------------------------------------
 *
 * RatDbGetMessage --
 *
 *      This routine extracts a copy of a message in the database and
 *	returns a MESSAGE structure.
 *
 * Results:
 *	A pointer to a MESSAGE* structure. It alse fills in the pointer
 *	to a buffer among the arguments with the address that needs to be
 *	freed when the message is deleted.
 *
 * Side effects:
 *      None.
 *
 *----------------------------------------------------------------------
 */

MESSAGE*
RatDbGetMessage(Tcl_Interp *interp, int index, char **buffer)
{
    char fname[1024];	/* Filename of message */
    int messfd;		/* Message file descriptor */
    struct stat sbuf;	/* Buffer for stat call (to find out size of message)*/
    char *message;	/* Pointer to actual message */

    /*
     * Check the index for validity.
     */
    if (index >= numRead || index < 0) {
	Tcl_SetResult(interp, "error: the given index is invalid", TCL_STATIC);
	return NULL;
    }
    if (NULL == entryPtr[index].content[FROM]) {
	Tcl_SetResult(interp, "error: the message is deleted", TCL_STATIC);
	return NULL;
    }

    Lock(interp);

    /*
     * Read the message into an array pointed to by 'message'.
     */
    snprintf(fname, sizeof(fname), "%s/dbase/%s",
	    dbDir, entryPtr[index].content[FILENAME]);
    if (0 > (messfd = open(fname, O_RDONLY))) {
	Unlock(interp);
	Tcl_AppendResult(interp, "error opening file (for read)\"",
		fname, "\": ", Tcl_PosixError(interp), (char*)NULL);
	return NULL;
    }
    if (0 != fstat(messfd, &sbuf)) {
	Unlock(interp);
	Tcl_AppendResult(interp, "error stating file \"", fname,
		"\": ", Tcl_PosixError(interp), (char *) NULL);
	close(messfd);
	return NULL;
    }
    *buffer = message = (char*)ckalloc(sbuf.st_size+1);
    read(messfd, message, sbuf.st_size);
    message[sbuf.st_size] = '\0';
    (void)close(messfd);

    Unlock(interp);

    return RatParseMsg(interp, (unsigned char*)message);
}


/*
 *----------------------------------------------------------------------
 *
 * RatDbGetHeaders --
 *
 *      This routine extracts a copy of the headers of a message in
 *	the database.
 *
 * Results:
 *      A pointer to a static area containing the message headers
 *	is returned.
 *
 * Side effects:
 *      None.
 *
 *----------------------------------------------------------------------
 */

char*
RatDbGetHeaders(Tcl_Interp *interp, int index)
{
    static char *header;	/* Static storage area */
    static int headerSize = 0;	/* Size of static storage area */
    char fname[1024];		/* Filename of message */
    char *hPtr;			/* The header to return */
    FILE *messFp;		/* Message file pointer */
    int length = 0;		/* Length of header */

    /*
     * Check the index for validity.
     */
    if (index >= numRead || index < 0) {
	Tcl_SetResult(interp, "error: the given index is invalid", TCL_STATIC);
	return NULL;
    }
    if (NULL == entryPtr[index].content[FROM]) {
	Tcl_SetResult(interp, "error: the message is deleted", TCL_STATIC);
	return NULL;
    }

    Lock(interp);

    /*
     * Read the message into an array pointed to by 'message'.
     */
    snprintf(fname, sizeof(fname), "%s/dbase/%s",
	    dbDir, entryPtr[index].content[FILENAME]);
    if (NULL == (messFp = fopen(fname, "r"))) {
	Unlock(interp);
	Tcl_AppendResult(interp, "error opening file (for read)\"",
		fname, "\": ", Tcl_PosixError(interp), (char*)NULL);
	return NULL;
    }
    headerSize = 8196;
    header = (char*)ckalloc(headerSize);
    while (fgets(header+length, headerSize-length, messFp), !feof(messFp)) {
	if (header[length] == '\n' || header[length] == '\r') {
	    if ('\n' == header[length+1]) {
		length += 2;
	    } else {
		length += 1;
	    }
	    break;
	}
	length += strlen(header+length);
	if (length >= headerSize-1) {
	    headerSize += 4096;
	    header = (char*)ckrealloc(header, headerSize);
	}
	if (length >= 2 && header[length-1] == '\n') {
	    if (header[length-2] != '\r') {
		header[length-1] = '\r';
		header[length++] = '\n';
	    }
	}
    }
    header[length] = '\0';
    fclose(messFp);
    Unlock(interp);
    if (strncmp("From ", header, 5)) {
	hPtr = header;
    } else {
	hPtr = strchr(header, '\n')+1;
	if ('\r' == *hPtr) {
	    hPtr++;
	}
    }
    return hPtr;
}


/*
 *----------------------------------------------------------------------
 *
 * RatDbGetFrom --
 *
 *      This routine extracts a copy of the first line of the headers
 *
 * Results:
 *      A pointer to a static area containing the from line
 *
 * Side effects:
 *      None.
 *
 *----------------------------------------------------------------------
 */

char*
RatDbGetFrom(Tcl_Interp *interp, int index)
{
    static char header[8192];	/* Static storage area */
    char fname[1024];		/* Filename of message */
    FILE *messFp;		/* Message file pointer */

    /*
     * Check the index for validity.
     */
    if (index >= numRead || index < 0) {
	Tcl_SetResult(interp, "error: the given index is invalid", TCL_STATIC);
	return NULL;
    }
    if (NULL == entryPtr[index].content[FROM]) {
	Tcl_SetResult(interp, "error: the message is deleted", TCL_STATIC);
	return NULL;
    }

    Lock(interp);

    /*
     * Read the message into an array pointed to by 'message'.
     */
    snprintf(fname, sizeof(fname), "%s/dbase/%s",
	    dbDir, entryPtr[index].content[FILENAME]);
    if (NULL == (messFp = fopen(fname, "r"))) {
	Unlock(interp);
	Tcl_AppendResult(interp, "error opening file (for read)\"",
		fname, "\": ", Tcl_PosixError(interp), (char*)NULL);
	return NULL;
    }
    Unlock(interp);
    fgets(header, sizeof(header)-1, messFp);
    fclose(messFp);
    header[sizeof(header)-1] = '\0';
    return header;
}



/*
 *----------------------------------------------------------------------
 *
 * RatDbGetText --
 *
 *      This routine extracts a copy of the body of a message in
 *	the database.
 *
 * Results:
 *      A pointer to a static area containing the message body
 *	is returned.
 *
 * Side effects:
 *      None.
 *
 *----------------------------------------------------------------------
 */

char*
RatDbGetText(Tcl_Interp *interp, int index)
{
    static char *body = NULL;	/* Static storage area */
    static int bodySize = 0;	/* Size of static storage area */
    char fname[1024];		/* Filename of message */
    FILE *messFp;		/* Message file pointer */
    int length = 0;		/* Length of header */
    char buf[2048];		/* Temporary holding area */

    /*
     * Check the index for validity.
     */
    if (index >= numRead || index < 0) {
	Tcl_SetResult(interp, "error: the given index is invalid", TCL_STATIC);
	return NULL;
    }
    if (NULL == entryPtr[index].content[FROM]) {
	Tcl_SetResult(interp, "error: the message is deleted", TCL_STATIC);
	return NULL;
    }

    Lock(interp);

    /*
     * Read the message into an array pointed to by 'message'.
     */
    snprintf(fname, sizeof(fname), "%s/dbase/%s",
	    dbDir, entryPtr[index].content[FILENAME]);
    if (NULL == (messFp = fopen(fname, "r"))) {
	Unlock(interp);
	Tcl_AppendResult(interp, "error opening file (for read)\"",
		fname, "\": ", Tcl_PosixError(interp), (char*)NULL);
	return NULL;
    }
    while (fgets(buf, sizeof(buf), messFp), !feof(messFp)) {
	if ('\n' == buf[0] || '\r' == buf[0]) {
	    break;
	}
    }
    if (0 == bodySize) {
	bodySize = 8196;
	body = (char*)ckalloc(bodySize);
    }
    while (fgets(body+length, bodySize-length, messFp), !feof(messFp)) {
	length += strlen(body+length);
	if (length >= bodySize-1) {
	    bodySize += 4096;
	    body = (char*)ckrealloc(body, bodySize);
	}
	if (length >= 2 && body[length-1] == '\n') {
	    if (body[length-2] != '\r') {
		body[length-1] = '\r';
		body[length++] = '\n';
	    }
	}
    }
    body[length] = '\0';
    fclose(messFp);
    Unlock(interp);
    return body;
}


/*
 *----------------------------------------------------------------------
 *
 * RatDbDelete --
 *
 *      Deletes the specified entry from the database.
 *
 * Results:
 *      The return value is normally TCL_OK; if something goes wrong
 *	TCL_ERROR is returned and an error message will be left in
 *	the result area.
 *
 * Side effects:
 *      Both the internal and the disk copy of the database are affected.
 *	Observer that if some caller has previously retrieved this entry
 *	from the database with a call to RatDbGet() the RatDbEntry
 *	obtained will be destroyed (filled with nulls).
 *
 *----------------------------------------------------------------------
 */

int
RatDbDelete(Tcl_Interp *interp, int index)
{
    char buf[1024];	/* Name of index.changes file */
    FILE *indexFP;	/* File pointer to index.changes file */

    /*
     * Check the index for validity.
     */
    if (index >= numRead || index < 0) {
	Tcl_SetResult(interp, "error: the given index is invalid", TCL_STATIC);
	return TCL_ERROR;
    }

    Lock(interp);

    snprintf(buf, sizeof(buf), "%s/index.changes", dbDir);
    if (NULL == (indexFP = fopen(buf, "a"))) {
	Tcl_AppendResult(interp, "error opening (for append)\"", buf,
		"\": ", Tcl_PosixError(interp), (char *) NULL);
	Unlock(interp);
	return TCL_ERROR;
    }
    if (0 > fprintf(indexFP, "d %d\n", index)) {
	Tcl_AppendResult(interp, "Failed to write to file \"", buf, "\"",
		(char*) NULL);
	(void)fclose(indexFP);
	Unlock(interp);
	return TCL_ERROR;
    }
    if (0 != fclose(indexFP)) {
	Tcl_AppendResult(interp, "error closing file \"", buf,
		"\": ", Tcl_PosixError(interp), (char *) NULL);
	Unlock(interp);
	return TCL_ERROR;
    }

    Sync(interp, 0);
    Unlock(interp);
    return TCL_OK;
}

/*
 *----------------------------------------------------------------------
 *
 * RatDbExpunge --
 *
 *      Deletes all entries marked for deletion (status contains D).
 *
 * Results:
 *      The return value is normally TCL_OK; if something goes wrong
 *	TCL_ERROR is returned and an error message will be left in
 *	the result area.
 *
 * Side effects:
 *      Both the internal and the disk copy of the database are affected.
 *	Observer that if some caller has previously retrieved this entry
 *	from the database with a call to RatDbGet() the RatDbEntry
 *	obtained will be destroyed (filled with nulls).
 *
 *----------------------------------------------------------------------
 */

int
RatDbExpunge(Tcl_Interp *interp)
{
    char buf[1024];	/* Name of index.changes file */
    FILE *indexFP;	/* File pointer to index.changes file */
    int index, i;

    Lock(interp);

    snprintf(buf, sizeof(buf), "%s/index.changes", dbDir);
    if (NULL == (indexFP = fopen(buf, "a"))) {
	Tcl_AppendResult(interp, "error opening (for append)\"", buf,
		"\": ", Tcl_PosixError(interp), (char *) NULL);
	Unlock(interp);
	return TCL_ERROR;
    }
    for (index=0; index < numRead; index++) {
	for (i=0; entryPtr[index].content[STATUS][i]; i++) {
	    if ('D' == entryPtr[index].content[STATUS][i]) {
		fprintf(indexFP, "d %d\n", index);
		break;
	    }
	}
    }
    if (0 != fclose(indexFP)) {
	Tcl_AppendResult(interp, "error closing file \"", buf,
		"\": ", Tcl_PosixError(interp), (char *) NULL);
	Unlock(interp);
	return TCL_ERROR;
    }

    Sync(interp, 0);
    Unlock(interp);
    return TCL_OK;
}


/*
 *----------------------------------------------------------------------
 *
 * RatDbDaysSinceExpire --
 *
 *	Finds ut how long it was since the database was expired last.
 *
 * Results:
 *	An integer which is the number of days since the database was expired.
 *	If the user has no database we return 0.
 *
 * Side effects:
 *      None.
 *
 *----------------------------------------------------------------------
 */

int
RatDbDaysSinceExpire(Tcl_Interp *interp)
{
    struct stat sbuf;
    char buf[1024];

    /*
     * First make sure we know where the database should reside.
     */
    if (0 == dbDir) {
	const char *value = RatGetPathOption(interp, "dbase_dir");
	if (NULL == value) {
	    return TCL_ERROR;
	}
	dbDir = cpystr(value);
    }

    snprintf(buf, sizeof(buf), "%s/expired", dbDir);
    if (stat(buf, &sbuf)) {
	snprintf(buf, sizeof(buf), "%s/dbase", dbDir);
	if (stat(buf, &sbuf)) {
	    return 0;
	}
    }
    if (sbuf.st_mtime > time(NULL)) {
	return 0;
    } else {
	return (time(NULL)-sbuf.st_mtime)/(24*60*60);
    }
}


/*
 *----------------------------------------------------------------------
 *
 * RatDbExpire --
 *
 *      Runs through the database and carries out any expiration that
 *	should be done. This routine should be called periodically.
 *
 * Results:
 *	If nothing went wrong TCL_OK is returned and in the result area is
 *	a list containing 5 numbers {num_scanned num_delete, num_backup,
 *	num_inbox num_custom}. Otherwise TCL_ERROR is returned.
 *
 * Side effects:
 *      None.
 *
 *----------------------------------------------------------------------
 */

int
RatDbExpire(Tcl_Interp *interp, char *infolder, char *backupDirectory)
{
    char *compressProg, *compressSuffix, *statusId;
    const char *backupDir;
    int numScan = 0, numDelete = 0, numBackup = 0, numInbox = 0, numCustom = 0;
    int i, len, delete, mode, fd, doBackup = 0, changed = 0, error = 0;
    int move;
    char buf[1024], buf2[1024];
    FILE *indexFP = NULL;
    time_t t, now = time(NULL);
    struct tm *tmPtr;
    struct stat sbuf;
    struct dirent *direntPtr;
    Tcl_Obj *oPtr;
    DIR *dirPtr;

    if (0 == isRead) {
	if (TCL_OK != Read(interp)) {
	    return TCL_ERROR;
	}
    }
    /*
     * Get file creation mode
     */
    oPtr = Tcl_GetVar2Ex(interp, "option", "permissions", TCL_GLOBAL_ONLY);
    Tcl_GetIntFromObj(interp, oPtr, &mode);
    /*
     * Make sure the inbox directory exists.
     */
    snprintf(buf, sizeof(buf), "%s/inbox", dbDir);
    if (-1 == stat(buf, &sbuf)) {
	if (mkdir(buf, DIRMODE)) {
	    Tcl_AppendResult(interp, "error creating\"", buf,
		    "\": ", Tcl_PosixError(interp), (char *) NULL);
	    return TCL_ERROR;
	}
    }

    /*
     * Prepare backup
     */
    if (-1 == stat(backupDirectory, &sbuf)) {
	if (mkdir(backupDirectory, DIRMODE)) {
	    Tcl_AppendResult(interp, "error creating\"", backupDirectory,
		    "\": ", Tcl_PosixError(interp), (char *) NULL);
	    return TCL_ERROR;
	}
    }
    compressProg = getenv("COMPRESS");
    compressSuffix = getenv("CSUFFIX");
    backupDir = RatGetPathOption(interp, "dbase_backup");
    if (!compressProg || !compressSuffix || !backupDir) {
	Tcl_AppendResult(interp, "Internal error: compressProg, ",
		"compressSuffix or option(dbase_backup) not defined",
		(char*) NULL);
	return TCL_ERROR;
    }

    RatLogF(interp, RAT_INFO, "db_expire", RATLOG_EXPLICIT);
    statusId = cpystr(Tcl_GetStringResult(interp));
    Lock(interp);
    for (i=0; !error && i < numRead; i++) {
	if (!entryPtr[i].content[FROM]) {	/* Entry deleted */
	    continue;
	}
	numScan++;
	t = atol(entryPtr[i].content[EX_TIME]);
	if (!t || t >now) {
	    continue;
	}

	/*
	 * If we get here the entry should be expired.
	 *
	 * Currently we only handle the backup, delete and inbox actions.
	 * The rest are quitely ignored.
	 */
	delete = move = 0;
	if (!strcmp("delete", entryPtr[i].content[EX_TYPE])) {
	    delete = 1;
	    numDelete++;

	} else if (!strcmp("backup", entryPtr[i].content[EX_TYPE])) {
	    move = 1;
	    numBackup++;
	    snprintf(buf, sizeof(buf), "%s/dbase/%s",
		    dbDir, entryPtr[i].content[FILENAME]);
	    RatGenId(NULL, interp, 0, NULL);
	    snprintf(buf2, sizeof(buf2), "%s/message.%s", backupDirectory, 
		    Tcl_GetStringResult(interp));

	} else if (!strcmp("incoming", entryPtr[i].content[EX_TYPE])) {
	    move = 1;
	    numInbox++;
	    snprintf(buf, sizeof(buf), "%s/dbase/%s",
		    dbDir, entryPtr[i].content[FILENAME]);
	    RatGenId(NULL, interp, 0, NULL);
	    snprintf(buf2, sizeof(buf2), "%s/inbox/%s",
		    dbDir, Tcl_GetStringResult(interp));

	} else if (!strncmp("custom", entryPtr[i].content[EX_TYPE], 6)) {
	    numCustom++;

	} else if (!strcmp("none", entryPtr[i].content[EX_TYPE])) {
	    continue;

	} else {
	    /*
	     * If we get here it is an unkown type and we just silently
	     * deletes it (old versions of tkrat may have generated it.
	     */
	    delete = 1;
	    numDelete++;
	}
	if (move) {
	    if (link(buf, buf2)) {
		int fdSrc, fdDst;
		/*
		 * Sigh. the files are on different filesystems. We have to
		 * copy them.
		 */
		fdSrc = open(buf, O_RDONLY);
		fdDst = open(buf, O_WRONLY|O_TRUNC|O_CREAT, mode);
		do {
		    len = read(fdSrc, buf, sizeof(buf));
		    if (0 > write(fdDst, buf, len)) {
			error = errno;
			len = 0;
		    }
		} while (len);
		close(fdSrc);
		if (close(fdDst) || error) {
		    RatLogF(interp, RAT_ERROR, "failed_to_move_to_file",
			    RATLOG_TIME, buf2,  Tcl_PosixError(interp));
		    delete = 0;
		}
	    }
	}
	if (delete || move) {
	    if (!indexFP) {
		changed = 1;
		snprintf(buf, sizeof(buf), "%s/index.changes", dbDir);
		if (NULL == (indexFP = fopen(buf, "a"))) {
		    Tcl_ResetResult(interp);
		    Tcl_AppendResult(interp, "error opening (for append)\"",
			    buf, "\":", Tcl_PosixError(interp), "\n", NULL);
		    Unlock(interp);
		    return TCL_ERROR;
		}
	    }
	    fprintf(indexFP, "d %d\n", i);
	}
    }
    if (changed) {
	fclose(indexFP);
	Sync(interp, 0);
    }
    Unlock(interp);

    /*
     * Compress the messages in the backup directory if we have enough
     * messages to make it meaningful.
     */
    if (numBackup) {
	int chunkSize;
	Tcl_Obj *oPtr;

	oPtr = Tcl_GetVar2Ex(interp, "option", "chunksize", TCL_GLOBAL_ONLY);
	if (oPtr) {
	    Tcl_GetIntFromObj(interp, oPtr, &chunkSize);
	} else {
	    chunkSize = 100;
	}

	if (numBackup < chunkSize) {
	    i = 0;
	    dirPtr = opendir(backupDirectory);
	    while (0 != (direntPtr = readdir(dirPtr))) {
		if (!strncmp(direntPtr->d_name, "message", 7)) {
		    i++;
		}
	    }
	    closedir(dirPtr);
	    doBackup = (i>=chunkSize);
	} else {
	    doBackup = 1;
	}
    }
    if (doBackup) {
	Tcl_Channel backupChannel, inChannel;
	CONST84 char *argv[3], *error = NULL;

	ckfree(statusId);
	RatLogF(interp, RAT_INFO, "packing_backup", RATLOG_EXPLICIT);
	statusId = cpystr(Tcl_GetStringResult(interp));
	tmPtr = localtime(&now);
	snprintf(buf2, sizeof(buf2),
		">%s/backup_%04d%02d%02d.%s", backupDirectory,
		tmPtr->tm_year+1900, tmPtr->tm_mon+1, tmPtr->tm_mday+1,
		compressSuffix);
	argv[0] = compressProg;
	argv[1] = buf2;
	if (!(backupChannel = Tcl_OpenCommandChannel(interp, 2, argv,
		TCL_STDIN))) {
	    Tcl_BackgroundError(interp);
	}
	if (backupChannel) {
	    dirPtr = opendir(backupDirectory);
	    while (!error && 0 != (direntPtr = readdir(dirPtr))) {
		if (strncmp(direntPtr->d_name, "message", 7)) {
		    continue;
		}
		snprintf(buf, sizeof(buf), "%s/%s",
			backupDirectory, direntPtr->d_name);
		inChannel = Tcl_OpenFileChannel(interp, buf, "r", 0);
		do {
		    len = Tcl_Read(inChannel, buf, sizeof(buf));
		    if (-1 == Tcl_Write(backupChannel, buf, len)) {
			error = Tcl_PosixError(interp);
		    }
		} while (!error && !Tcl_Eof(inChannel));
		Tcl_Write(backupChannel, "\n", 1);
		Tcl_Close(interp, inChannel);
	    }
	    if (TCL_OK != Tcl_Close(interp, backupChannel)) {
		error = Tcl_PosixError(interp);
	    }
	    if (error) {
		unlink(buf2);
		Tcl_SetResult(interp, (char*)error, TCL_STATIC);
		Tcl_BackgroundError(interp);
	    } else {
		rewinddir(dirPtr);
		while (0 != (direntPtr = readdir(dirPtr))) {
		    if (!strncmp(direntPtr->d_name, "message", 7)) {
			snprintf(buf, sizeof(buf), "%s/%s",
				backupDirectory, direntPtr->d_name);
			unlink(buf);
		    }
		}
	    }
	    closedir(dirPtr);
	}
    }

    /*
     * Move messages to inbox
     */
    if (numInbox) {
	char *data = NULL, *msg, *cPtr;
	int fd, allocated = 0;

	snprintf(buf, sizeof(buf), "%s/inbox", dbDir);
	dirPtr = opendir(buf);
	while (0 != (direntPtr = readdir(dirPtr))) {
	    snprintf(buf2, sizeof(buf2), "%s/%s", buf, direntPtr->d_name);
	    if (stat(buf2, &sbuf) || !S_ISREG(sbuf.st_mode)) {
		continue;
	    }
	    if (allocated < sbuf.st_size+1) {
		allocated = sbuf.st_size+1;
		data = (char*)ckrealloc(data, allocated);
	    }
	    fd = open(buf2, O_RDONLY);
	    read(fd, data, sbuf.st_size);
	    close(fd);
	    data[sbuf.st_size] = '\0';
	    unlink(buf2);
	    for (cPtr = data; *cPtr != '\n'; cPtr++);
	    if (*(++cPtr) == '\r') {
		cPtr++;
	    }
	    msg = RatFrMessageCreate(interp, cPtr, sbuf.st_size, NULL);
	    if (TCL_OK == Tcl_VarEval(interp, infolder, " insert ", msg,NULL)){
		unlink(buf2);
	    }
	}
	closedir(dirPtr);
    }
    /*
     * Mark that we have done expire
     */
    snprintf(buf, sizeof(buf), "%s/expired", dbDir);
    (void)unlink(buf);
    if (0 <= (fd = open(buf, O_WRONLY|O_CREAT, mode))) {
	close(fd);
    }
    Tcl_VarEval(interp, "RatClearLog ", statusId, "; update idletasks",
	    (char*) NULL);
    ckfree(statusId);

    /*
     * Create result list and return
     */
    oPtr = Tcl_NewObj();
    Tcl_ListObjAppendElement(interp, oPtr, Tcl_NewIntObj(numScan));
    Tcl_ListObjAppendElement(interp, oPtr, Tcl_NewIntObj(numDelete));
    Tcl_ListObjAppendElement(interp, oPtr, Tcl_NewIntObj(numBackup));
    Tcl_ListObjAppendElement(interp, oPtr, Tcl_NewIntObj(numInbox));
    Tcl_ListObjAppendElement(interp, oPtr, Tcl_NewIntObj(numCustom));
    Tcl_SetObjResult(interp, oPtr);
    return TCL_OK;
}

/*
 *----------------------------------------------------------------------
 *
 * RatDbClose --
 *
 *      Closes the database on disk.
 *
 * Results:
 *      None.
 *
 * Side effects:
 *      The rlock file is removed and some of the internal data structures
 *	are freed (but not all :-().
 *
 *----------------------------------------------------------------------
 */

void
RatDbClose()
{
    char buf[1024];	/* Scratch area */

    if (1 == isRead) {
	ckfree(entryPtr);
	isRead = 0;

	snprintf(buf, sizeof(buf), "%s/rlock.%s", dbDir, ident);
	unlink(buf);
    }
#ifdef MEM_DEBUG
    ckfree(dbDir);
#endif /* MEM_DEBUG */
}


/*
 *----------------------------------------------------------------------
 *
 * RatDbBuildList --
 *
 *      Builds a list of the files in the database
 *
 *	The algorithm is to open the directory and for all files do:
 *	  - If the file is ".seq" then we read it and remember the number
 *	  - If the name starts with a dot ('.') then we continue with the
 *	    next file.
 *	  - If it is a directory the we recursively call ourselves to check
 *	    that directory.
 *	  - If it is an ordinary file then we add it to the hastable (the
 *	    hash is computed from the prefix/filename). We also decode the
 *	    number of the file and remembers the highest number found.
 *	When all files are checked we check if the highest number found
 *	was bigger than the content of seq. If it is so then we
 *	  - Append a warning to the result area.
 *	  - write a new .seq file IF fix is true.
 *
 * Results:
 *	May append diagnostic messages to the result area.
 *
 * Side effects:
 *	Will probably add items to the given hashtable.
 *
 *----------------------------------------------------------------------
 */

static void
RatDbBuildList(Tcl_Interp *interp, Tcl_DString *dsPtr, char *prefix, char *dir,
	Tcl_HashTable *tablePtr, int fix)
{
    char buf[1024], path[1024];
    int seq = -1, i;
    Tcl_HashEntry *entryPtr;
    RatDbItem *itemPtr;
    struct dirent *entPtr;
    struct stat sbuf;
    DIR *dirPtr;
    FILE *fp;

    if (NULL == (dirPtr = opendir(dir))) {
	snprintf(buf, sizeof(buf), "Failed to open directory \"%s\": %s", dir,
		Tcl_PosixError(interp));
	Tcl_DStringAppendElement(dsPtr, buf);
	return;
    }
    while (NULL != (entPtr = readdir(dirPtr))) {
	if (!strcmp(entPtr->d_name, ".seq")) {
	    snprintf(path, sizeof(buf), "%s/.seq", dir);
	    if (NULL == (fp = fopen(path, "r"))) {
		snprintf(buf, sizeof(buf), "Failed to open file \"%s\": %s",
			path, Tcl_PosixError(interp));
		Tcl_DStringAppendElement(dsPtr, buf);
		if (fix) {
		    if (unlink(path)) {
			snprintf(buf, sizeof(buf),
				"Failed to unlink file \"%s\": %s", path,
				Tcl_PosixError(interp));
			Tcl_DStringAppendElement(dsPtr, buf);
		    }
		}
	    } else {
		fscanf(fp, "%d", &seq);
		fclose(fp);
	    }
	}
	if ('.' == entPtr->d_name[0]) {
	    continue;
	}
	snprintf(path, sizeof(path), "%s/%s", dir, entPtr->d_name);
	if (stat(path, &sbuf)) {
	    snprintf(buf, sizeof(buf), "Failed to stat file %s: %s\n", path,
		    Tcl_PosixError(interp));
	    Tcl_DStringAppendElement(dsPtr, buf);
	    continue;
	}
	if (S_IFREG == (sbuf.st_mode&S_IFMT)) {
	    if (!(S_IRUSR & sbuf.st_mode)) {
		snprintf(buf, sizeof(buf),
			"\"%s\" is not readable by the owner", path);
		Tcl_DStringAppendElement(dsPtr, buf);
		if (fix) {
		    if (chmod(path, sbuf.st_mode|S_IRUSR)) {
			snprintf(buf, sizeof(buf),
				"Failed to chmod \"%s\": %s", path,
				Tcl_PosixError(interp));
			Tcl_DStringAppendElement(dsPtr, buf);
			continue;
		    }
		}
	    }
	    if (0 == sbuf.st_size) {
		snprintf(buf, sizeof(buf), "Empty file \"%s\" found", path);
		if (fix) {
		    if (unlink(path)) {
			snprintf(buf, sizeof(buf),
				"Failed to unlink \"%s\": %s", path,
				Tcl_PosixError(interp));
			Tcl_DStringAppendElement(dsPtr, buf);
		    }
		}
		continue;
	    }
	    if (*prefix) {
		snprintf(buf, sizeof(buf), "%s/%s", prefix, entPtr->d_name);
	    } else {
		strlcpy(buf, entPtr->d_name, sizeof(buf));
	    }
	    itemPtr = (RatDbItem*)ckalloc(sizeof(RatDbItem));
	    itemPtr->fileSize = sbuf.st_size;
	    itemPtr->index = -1;
	    for (i=0; i<RATDBETYPE_END; i++) {
		itemPtr->entry.content[i] = NULL;
	    }
	    entryPtr = Tcl_CreateHashEntry(tablePtr, buf, &i);
	    Tcl_SetHashValue(entryPtr, (ClientData)itemPtr);
	} else if (S_IFDIR == (sbuf.st_mode&S_IFMT)) {
	    if (prefix && *prefix) {
		snprintf(path, sizeof(path), "%s/%s", prefix, entPtr->d_name);
	    } else {
		strlcpy(path, entPtr->d_name, sizeof(path));
	    }
	    snprintf(buf, sizeof(buf), "%s/%s", dir, entPtr->d_name);
	    RatDbBuildList(interp, dsPtr, path, buf, tablePtr, fix);
	} else {
	    snprintf(buf, sizeof(buf), "\"%s\" is not a file", path);
	    Tcl_DStringAppendElement(dsPtr, buf);
	}
    }
    closedir(dirPtr);
}


/*
 *----------------------------------------------------------------------
 *
 * RatDbCheck --
 *
 *      Checks the database.
 *
 * Results:
 *	A diagnostic string.
 *
 * Side effects:
 *	The database on disk may be rewritten (depends on the fix argument).
 *
 *----------------------------------------------------------------------
 */

#define EXP_NUM		"[0-9]*"
#define EXP_TYPE	"^((none)|(remove)|(incoming)|(backup)|(custom.*))?$"
#define EXP_FILE	"[^/]+/[0-9]*"

int
RatDbCheck(Tcl_Interp *interp, int fix)
{
    int numFound = 0, numMal = 0, numAlone = 0, numUnlinked = 0, totSize = 0,
        fd, lines, start, index, i, j, extraNum = 0, extraAlloc = 0,
	msgLen = 0, date = 0, listArgc, elemArgc, indexInfo = 0, numDel = 0;
    char buf[8092], *indexPtr = NULL, **linePtrPtr = NULL, *cPtr,
	 *to, *from, *cc, *subject, *flags, *msgBuf = NULL;
    CONST84 char **listArgv, **elemArgv;
    Tcl_HashTable items, status;
    char **extraPtrPtr = NULL;
    Tcl_HashEntry *entryPtr;
    Tcl_HashSearch search;
    Tcl_DString reportDS;
    RatDbItem *itemPtr;
    struct stat sbuf;
    MESSAGECACHE elt;
    struct tm tm;
    FILE *fp;

    /*
     * Initialize variables
     */
    if (0 == dbDir) {
	const char *value = RatGetPathOption(interp, "dbase_dir");
	if (NULL == value) {
	    return TCL_ERROR;
	}
	dbDir = cpystr(value);
    }
    if (0 == ident) {
	gethostname(buf, sizeof(buf));
	ident = (char*)ckalloc(strlen(buf)+16);
	snprintf(ident, strlen(buf)+16, "%s:%d", buf, (int)getpid());
    }

    /*
     * Check that the database directory exists. If not we return zeros
     */
    if (0 > stat(dbDir, &sbuf) ||
	    !S_ISDIR(sbuf.st_mode)) {
	Tcl_SetResult(interp, "0 0 0 0 0 {}", TCL_STATIC);
	return TCL_OK;
    }


    /*
     * Lock the database. We should also check that nobody else has
     * a read lock as well.
     */
    Lock(interp);
    if (IsRlocked(NULL)) {
	Unlock(interp);
	Tcl_SetResult(interp, "Some other process has locked the database.",
		TCL_STATIC);
	return TCL_ERROR;
    }

    /*
     * Check index.info file
     */
    snprintf(buf, sizeof(buf), "%s/index.info", dbDir);
    if (NULL == (fp = fopen(buf, "r"))) {
	Tcl_SetResult(interp, "Failed to open index.info file", TCL_STATIC);
	return TCL_ERROR;
    } else {
	fscanf(fp, "%d %d", &i, &indexInfo);
	fclose(fp);
	if (i != DBASE_VERSION) {
	    Tcl_SetResult(interp, "Wrong version of dbase", TCL_STATIC);
	    Unlock(interp);
	    return TCL_ERROR;
	}
    }

    /*
     * Initialize variables
     */
    Tcl_DStringInit(&reportDS);
    Tcl_InitHashTable(&items, TCL_STRING_KEYS);
    Tcl_InitHashTable(&status, TCL_ONE_WORD_KEYS);

    /*
     * Get a list of messages actually stored
     */
    snprintf(buf, sizeof(buf), "%s/dbase", dbDir);
    RatDbBuildList(interp, &reportDS, "", buf, &items, fix);

    /*
     * Check the changes file for flag changes and store them in the
     * status hash table.
     */
    snprintf(buf, sizeof(buf), "%s/index.changes", dbDir);
    if (NULL != (fp = fopen(buf, "r"))) {
	while (fgets(buf, sizeof(buf), fp), !feof(fp)) {
	    switch (buf[0]) {
	    case 'a':
		indexInfo++;
		break;
	    case 'd':
		indexInfo--;
		numDel++;
		break;
	    case 's':
		if (extraNum == extraAlloc) {
		    extraAlloc += 32;
		    extraPtrPtr = (char**)ckrealloc(extraPtrPtr,
			    extraAlloc*sizeof(char*));
		}
		extraPtrPtr[extraNum] = (char*)ckalloc(strlen(buf));
		sscanf(buf, "%*s %d %s", &index, extraPtrPtr[extraNum]);
		entryPtr = Tcl_CreateHashEntry(&status, (char*)index, &i);
		Tcl_SetHashValue(entryPtr, (ClientData)extraPtrPtr[extraNum]);
		extraNum++;
		break;
	    }
	}
	fclose(fp);
    }

    /*
     * Check the index file
     */
    snprintf(buf, sizeof(buf), "%s/index", dbDir);
    if (-1 != (fd = open(buf, O_RDONLY))) {
	/*
	 * Read file and build pointers to the lines
	 */
	fstat(fd, &sbuf);
	indexPtr = (char*)ckalloc(sbuf.st_size+1);
	read(fd, indexPtr, sbuf.st_size);
	close(fd);
	indexPtr[sbuf.st_size] = '\0';
	for (lines = 0, cPtr = indexPtr; *cPtr; cPtr++) {
	    if ('\n' == *cPtr) {
		lines++;
	    }
	}
	linePtrPtr = (char**)ckalloc(sizeof(char*)*lines);
	for (cPtr = indexPtr, i = 0; cPtr && *cPtr && i < lines; i++) {
	    linePtrPtr[i] = cPtr;
	    if ((cPtr = strchr(cPtr, '\n'))) {
		*cPtr++ = '\0';
	    }
	}

	/*
	 * Now we are ready to reconstruct the index. We do this one entry
	 * a time. For each entry we read one line at a time and check the
	 * content against what we expected. We expect the following lines
	 * and contents:
	 *	to	   - any string
	 *	from	   - any string
	 *	cc	   - any string
	 *	message-id - any string
	 *	references - any string
	 *	subject	   - any string
	 *	date	   - a number
	 *	keywords   - any string
	 *	size	   - a number
	 *	status	   - any string
	 *	extime	   - a number
	 *	exevent	   - one of none, remove, incoming, backup and custom
	 *	filename   - (.*)/[0-9]+
	 * When we have found an index we check for the corresponding entry
	 * in the list of files and fill it in.
	 */
	for (start = index = 0; start < lines; index++) {
	    if (start > lines-RATDBETYPE_END) {
		break;
	    }
	    if (!Tcl_RegExpMatch(interp, linePtrPtr[start+DATE], EXP_NUM)
		    || !Tcl_RegExpMatch(interp, linePtrPtr[start+RSIZE],
					EXP_NUM)
		    || !Tcl_RegExpMatch(interp, linePtrPtr[start+EX_TIME],
					EXP_NUM)
		    || !Tcl_RegExpMatch(interp, linePtrPtr[start+EX_TYPE],
					EXP_TYPE)
		    || !Tcl_RegExpMatch(interp, linePtrPtr[start+FILENAME],
					EXP_FILE)){
		sprintf(buf, "Entry %d is malformed", index);
		Tcl_DStringAppendElement(&reportDS, buf);
		numMal++;

		/*
		 * We have found an malformed entry, first we search for the
		 * filename which should be the last item. From that we go
		 * backwards and try to collapse lines that somehow was
		 * splitted.
		 */
		i=0;
		while (!Tcl_RegExpMatch(interp,linePtrPtr[start+i],EXP_FILE)) {
		    if (start + (++i) == lines) {
			break;
		    };
		}
		i++;
		if (start+i >= lines) {
		    /*
		     * We have reached the end of the file
		     */
		    break;
		}

		/* 
		 * Here we should collapse the lines but for now we
		 * just continue with the next item. /MaF
		 */
		start += i;
		continue;

	    }
	    if (!(entryPtr =
		    Tcl_FindHashEntry(&items, linePtrPtr[start+FILENAME]))) {
		numAlone++;
		snprintf(buf, sizeof(buf),
			"Entry %d has no associated file '%s'",
			index, linePtrPtr[start+FILENAME]);
		Tcl_DStringAppendElement(&reportDS, buf);
		start += 13;
		continue;
	    }
	    itemPtr = (RatDbItem*)Tcl_GetHashValue(entryPtr);
	    for (i=0; i<RATDBETYPE_END; i++, start++) {
		if (i == STATUS && (entryPtr = Tcl_FindHashEntry(&status,
			(char*)numFound))) {
		    itemPtr->entry.content[i] =
			(char*)Tcl_GetHashValue(entryPtr);
		} else {
		    itemPtr->entry.content[i] = linePtrPtr[start];
		}
	    }
	    numFound++;
	}
    }

    /*
     * Check for unlinked messages
     * And calculate total size while we are at it.
     */
    for (entryPtr = Tcl_FirstHashEntry(&items, &search);
	    entryPtr;
	    entryPtr = Tcl_NextHashEntry(&search)) {
	itemPtr = (RatDbItem*)Tcl_GetHashValue(entryPtr);
	totSize += itemPtr->fileSize;
	if (itemPtr->entry.content[0]) {
	    continue;
	}
	numUnlinked++;
	if (fix) {
	    /*
	     * Generate index entries for this message
	     */
	    to = from = cc = subject = flags = NULL;
	    date = 0;
	    if (extraNum+8 >= extraAlloc) {
		extraAlloc += 32;
		extraPtrPtr = (char**)ckrealloc(extraPtrPtr,
			extraAlloc*sizeof(char*));
	    }
	    if (msgLen < itemPtr->fileSize+1) {
		msgLen = itemPtr->fileSize+4096;
		msgBuf = (char*)ckrealloc(msgBuf, msgLen);
	    }
	    snprintf(buf, sizeof(buf), "%s/dbase/%s",
		    dbDir, Tcl_GetHashKey(&items,entryPtr));
	    if (-1 == (fd = open(buf, O_RDONLY))) {
		continue;
	    }
	    read(fd, msgBuf, itemPtr->fileSize);
	    msgBuf[itemPtr->fileSize] = '\0';
	    close(fd);
	    if (NULL == (cPtr = strstr(msgBuf, "\n\n"))) {
		if (NULL == (cPtr = strstr(msgBuf, "\r\n\r"))) {
		    cPtr = msgBuf + strlen(msgBuf);
		}
	    }
	    *(++cPtr) = '\0';
	    RatMessageGetHeader(interp, msgBuf);
	    Tcl_SplitList(interp, Tcl_GetStringResult(interp),
		    &listArgc, &listArgv);
	    for (i=0; i<listArgc; i++) {
		Tcl_SplitList(interp, listArgv[i], &elemArgc, &elemArgv);
		if (!to && !strcasecmp(elemArgv[0], "to")) {
		    to = extraPtrPtr[extraNum++] = cpystr(elemArgv[1]);
		} else if (!from && !strcasecmp(elemArgv[0], "from")) {
		    from = extraPtrPtr[extraNum++] = cpystr(elemArgv[1]);
		} else if (!cc && !strcasecmp(elemArgv[0], "cc")) {
		    cc = extraPtrPtr[extraNum++] = cpystr(elemArgv[1]);
		} else if (!subject && !strcasecmp(elemArgv[0], "subject")) {
		    subject = extraPtrPtr[extraNum++] = cpystr(elemArgv[1]);
		} else if (!strcasecmp(elemArgv[0], "status") ||
			   !strcasecmp(elemArgv[0], "x-status")) {
		    if (flags) {
			flags=(char*)ckrealloc(flags,strlen(flags)+
				strlen(elemArgv[1])+1);
			strcpy(&flags[strlen(flags)], elemArgv[1]);
		    } else {
			flags = cpystr(elemArgv[1]);
		    }
		} else if (!strcasecmp(elemArgv[0], "date")) {
		    if (T == mail_parse_date(&elt, (char*)elemArgv[1])) {
			tm.tm_sec = elt.seconds;
			tm.tm_min = elt.minutes;
			tm.tm_hour = elt.hours;
			tm.tm_mday = elt.day;
			tm.tm_mon = elt.month - 1;
			tm.tm_year = elt.year+70;
			tm.tm_wday = 0;
			tm.tm_yday = 0;
			tm.tm_isdst = -1;
			date = (int)mktime(&tm);
		    } else {
			date = 0;
		    }
		}
		ckfree(elemArgv);
	    }
	    ckfree(listArgv);
	    if (flags) {
		extraPtrPtr[extraNum++] = flags;
	    }
	    itemPtr->entry.content[TO] = to ? to : "";
	    itemPtr->entry.content[FROM] = from ? from : "";
	    itemPtr->entry.content[CC] = cc ? cc : "";
	    itemPtr->entry.content[SUBJECT] = subject ? subject : "";
	    sprintf(buf, "%d", date);
	    itemPtr->entry.content[DATE] = extraPtrPtr[extraNum++]=cpystr(buf);
	    itemPtr->entry.content[KEYWORDS] = "LostMessage";
	    sprintf(buf, "%d", itemPtr->fileSize);
	    itemPtr->entry.content[RSIZE] =extraPtrPtr[extraNum++]=cpystr(buf);
	    itemPtr->entry.content[STATUS] = flags ? flags : "";
	    sprintf(buf, "%ld", time(NULL) + 60L*60L*24L*100L);
	    itemPtr->entry.content[EX_TIME] = extraPtrPtr[extraNum++] =
		cpystr(buf);
	    itemPtr->entry.content[EX_TYPE] = "backup";
	    itemPtr->entry.content[FILENAME] = Tcl_GetHashKey(&items,entryPtr);
	}
    }
    if (numUnlinked && fix) {
	Tcl_DStringAppendElement(&reportDS,
"The unlinked messages has been inserted with the keyword 'LostMessage'");
    }
    if (indexInfo != numFound+numUnlinked-numDel) {
	if (fix) {
	    sprintf(buf, "Number of messages in index.info was wrong "
		    "(was: %d is now: %d)",
		indexInfo, numFound+numUnlinked-numDel);
	} else {
	    sprintf(buf, "Number of messages in index.info is wrong "
		    "(was: %d should be: %d)",
		indexInfo, numFound+numUnlinked-numDel);
	}
	Tcl_DStringAppendElement(&reportDS, buf);
    }

    /*
     * Write new index if fixing and needing
     */
    if (fix && (numMal || numAlone || numUnlinked ||
	    indexInfo != numFound+numUnlinked-numDel)) {
	snprintf(buf, sizeof(buf), "%s/index", dbDir);
	fp = fopen(buf, "w");
	for (entryPtr = Tcl_FirstHashEntry(&items, &search), j=0;
		entryPtr;
		entryPtr = Tcl_NextHashEntry(&search)) {
	    itemPtr = (RatDbItem*)Tcl_GetHashValue(entryPtr);
	    for (i=0; i<RATDBETYPE_END; i++) {
		if (itemPtr->entry.content[i]) {
		    fputs(itemPtr->entry.content[i], fp);
		}
		fputc('\n', fp);
	    }
	    j++;
	}
	fclose(fp);
	snprintf(buf, sizeof(buf), "%s/index.info", dbDir);
	fp = fopen(buf, "w");
	fprintf(fp, "%d %d\n", DBASE_VERSION, j);
	fclose(fp);
	snprintf(buf, sizeof(buf), "%s/index.changes", dbDir);
	(void)unlink(buf);

	if (isRead) {
	    isRead = 0;
	    strlcpy(buf, "Popup $t(need_restart)", sizeof(buf));
	    Tcl_Eval(interp, buf);
	}
    }
    
    /*
     * Cleaning up
     */
    Unlock(interp);

    for (entryPtr = Tcl_FirstHashEntry(&items, &search);
	    entryPtr;
	    entryPtr = Tcl_NextHashEntry(&search)) {
	ckfree(Tcl_GetHashValue(entryPtr));
    }
    Tcl_DeleteHashTable(&items);
    Tcl_DeleteHashTable(&status);
    ckfree(indexPtr);
    ckfree(linePtrPtr);

    Tcl_ResetResult(interp);
    sprintf(buf, "%d", numFound);
    Tcl_AppendElement(interp, buf);
    sprintf(buf, "%d", numMal);
    Tcl_AppendElement(interp, buf);
    sprintf(buf, "%d", numAlone);
    Tcl_AppendElement(interp, buf);
    sprintf(buf, "%d", numUnlinked);
    Tcl_AppendElement(interp, buf);
    sprintf(buf, "%d", totSize);
    Tcl_AppendElement(interp, buf);
    Tcl_AppendElement(interp, Tcl_DStringValue(&reportDS));
    Tcl_DStringFree(&reportDS);
    if (extraAlloc) {
	for (i=0; i<extraNum; i++) {
	    ckfree(extraPtrPtr[i]);
	}
	ckfree(extraPtrPtr);
    }
    return TCL_OK;
}


/*
 *----------------------------------------------------------------------
 *
 * NoLFPrint --
 *
 *      Prints the given string, but replaces any newlines with space.
 *	Also handles null strings.
 *
 * Results:
 *	-1 if any error ocurred
 *
 * Side effects:
 *	The mentioned file is modified
 *
 *----------------------------------------------------------------------
 */

static int
NoLFPrint(FILE *fp, const char *s)
{
    unsigned char *cPtr;

    for (cPtr = (unsigned char*)s; cPtr && *cPtr; cPtr++) {
	if ('\n' == *cPtr) {
	    if (isspace(cPtr[1])) {
		cPtr++;
	    } else {
		fputc(' ', fp);
	    }
	} else {
	    fputc(*cPtr, fp);
	}
    }
    return fputc('\n', fp);
}


/*
 *----------------------------------------------------------------------
 *
 * DbaseConvert3to4 --
 *
 *      Convert version 3 of the database to version 4
 *
 * Results:
 *	None
 *
 * Side effects:
 *	The databse index is rewritten
 *
 *----------------------------------------------------------------------
 */

static void
DbaseConvert3to4(Tcl_Interp *interp)
{
    char buf[1024];		/* Scratch area */
    char oldIndex[1024];	/* Name of old index file */
    char newIndex[1024];	/* Name of new index file */
    FILE *fpNewIndex;		/* File pointer to new index file */
    FILE *fpIndexinfo;		/* File pointer to new index.info file */
    Tcl_DString ds;		/* String to store converted texts in */
    int i, j;			/* Loop variables */
    char *s;			/* Scratch string pointer */
    int p, p2;			/* percentage counters */
    int numEntries = 0;		/* Number of entries in written file */

    RatLogF(interp, RAT_INFO, "converting_dbase", RATLOG_EXPLICIT, 0);
    strcpy(buf, "update idletasks");
    Tcl_Eval(interp, buf);

    snprintf(oldIndex, sizeof(oldIndex), "%s/index", dbDir);
    snprintf(newIndex, sizeof(newIndex), "%s/index.new", dbDir);
    if (0 == (fpNewIndex = fopen(newIndex, "w"))) {
	return;
    }

    Tcl_DStringInit(&ds);
    for (i=p=0,p2=-1; i < numRead; i++) {
	p = (i*100)/numRead;
	if (p != p2) {
	    RatLogF(interp, RAT_INFO, "converting_dbase", RATLOG_EXPLICIT,
		    (i*100)/numRead);
	    strcpy(buf, "update idletasks");
	    Tcl_Eval(interp, buf);
	    p2 = p;
	}
	if (0 != entryPtr[i].content[FROM]) {
	    numEntries++;
	    for (j=0; j<RATDBETYPE_END; j++) {
		for (s = entryPtr[i].content[j]; *s && !(0x80 & *s); s++);
		if (*s) {
		    Tcl_DStringSetLength(&ds, 0);
		    Tcl_ExternalToUtfDString(NULL, entryPtr[i].content[j], -1,
			    &ds);
		    s = Tcl_DStringValue(&ds);
		} else if (TO == j || FROM == j || CC == j || SUBJECT == j) {
		    s = RatDecodeHeader(interp,  entryPtr[i].content[j],
			    SUBJECT != j);
		} else {
		    s =  entryPtr[i].content[j];
		}
		if (0 > fprintf(fpNewIndex, "%s\n", s)) {
		    return;
		}
	    }
	}
    }
    fclose(fpNewIndex);
    rename(newIndex, oldIndex);
    snprintf(buf, sizeof(buf), "%s/index.info", dbDir);
    if (0 == (fpIndexinfo = fopen(buf, "w"))
	    || (0 > fprintf(fpIndexinfo, "%d %d\n", DBASE_VERSION, numEntries))
	    || (0 > fclose(fpIndexinfo))) {
	return;
    }
    snprintf(buf, sizeof(buf), "%s/index.changes", dbDir);
    unlink(buf);
    isRead = 0;
    ckfree(entryPtr[0].content[0]);
    ckfree(entryPtr);

    RatLog(interp, RAT_INFO, "", RATLOG_EXPLICIT);
}


syntax highlighted by Code2HTML, v. 0.9.1