/*
 * Copyright (c) 1999 Ian Freislich
 * All rights 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, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``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 OR CONTRIBUTORS 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.
 *
 *      $Id: mbox_mbf.c,v 1.20 2003/03/03 12:10:19 ianf Exp $
 */

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/utsname.h>
#include <netinet/in.h>

#ifdef SOLARIS
#include "/usr/ucbinclude/fcntl.h"
#endif
#include <ctype.h>
#include <db.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <md5.h>
#include <poll.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sysexits.h>
#include <syslog.h>
#include <time.h>
#include <unistd.h>

#include "poputil.h"
#include "private.h"

#define MBF_F_OK        0x0001
#define MBF_F_NOSUCH    0x0002
#define MBF_F_DELETE    0x0004
#define MBF_F_EXPIRE    0x0008
#define MBF_F_REMOVE    0x0010
#define MBF_F_READ      0x0020
#define MBF_F_TOP       0x0040
#define MBF_F_STAT      0x0080

/* Need this global variable to get configuration parameters to the compar()
 * routine
 */
static int	flags;

struct message {
	time_t	d_time;
	off_t	offset;
	char	uidl[33];
	char	*header;
	size_t	bytes;
	int	flags;
};

struct mbox {
	int		fd;
	size_t		bytes;
	int		count;
	int		max;
	int		expire;
	int		remove;
	struct message	*msg;
};

static int	compar(const void *, const void *);
static void	moremessages(struct mbox *);

/* Read the mailbox and rebuild the index file.
 */
static int
mbf_open(struct mbox *mbox, struct connection *cxn)
{
	int			len, offset, msgs, topoffile, lastblank,
				addheader, headerlen = 0;
	char			buffer[MAXBUFLEN + 1], uidldat[MAXBUFLEN + 1],
				*line, *p, *q, *r;
	unsigned char		digest[16];
	struct tm		tm;
	time_t			now;
	size_t			buffleft, bytes, size, uidlen;
	struct stat		stat_s;
	MD5_CTX			context;

	memset(mbox, '\0', sizeof(struct mbox));
	if (stat(cxn->mailpath, &stat_s) < 0){
		mbox->count = -1;
		mbox->bytes = 0;
		mbox->fd = -1;
		return(TRUE);
	}
	mbox->fd = openlock(cxn->mailpath, O_RDWR|O_NONBLOCK);
	if (mbox->fd < 0)
		return(FALSE);

	size = bytes = (size_t)stat_s.st_size;

	memset(buffer, NULL, MAXBUFLEN);
	p = q = buffer;
	topoffile = TRUE;
	lastblank = FALSE;
	addheader = FALSE;
	msgs = -1;
	offset = 0;
	len = 0;

	/* There really doesn't seem to be any easy or more intuitive way
	 * to get the system timezone and daylight savings information
	 * than this round-about method.
	 * ctime sucks!
	 */
	now = time(NULL);
	memcpy(&tm, localtime(&now), sizeof(struct tm));
	for (;;) {
		line = p;
		if ((p = strchr(line, '\n')) == NULL) {
			if (bytes == 0)
				break;
			strcpy(buffer, line);
			line = buffer;
			q = strchr(buffer, '\0');
			buffleft = MAXBUFLEN - (q - buffer) - 1;
			if (buffleft > bytes)
				buffleft = bytes;
			len = read(mbox->fd, q, buffleft);
			bytes -= len;
			offset += len;
			q[buffleft] = '\0';
			p = strchr(buffer, '\n');
		}
		*p++ = '\0';
		if (*line == '\0') {
			lastblank = TRUE;
			addheader = FALSE;
			continue;
		}
		if (addheader) {
			headerlen += strlen(line);
			mbox->msg[msgs].header = realloc(mbox->msg[msgs].header,
			    headerlen + 1);
			strcat(mbox->msg[msgs].header, line);
		}
		if ((topoffile || lastblank ) && !strncmp(line, "From ", 5)) {
			topoffile = FALSE;
			addheader = TRUE;
			if (msgs + 1 >= mbox->max)
				moremessages(mbox);
			msgs++;
			mbox->msg[msgs].header = NULL;
			mbox->msg[msgs].flags = 0;
			mbox->msg[msgs].offset = offset -
			    (len - (line - buffer)) - (q - buffer);
			if (!((r = memchr(line, ' ', (size_t)(line - p))) &&
			    r++ &&
			    (r = memchr(r, ' ', (size_t)(q - r))) && r++ ))
				r = p;
			if (!strptime(r, "%a %b %d %T %Y", &tm))
				tm = *(localtime(&now));
			mbox->msg[msgs].d_time = mktime(&tm);
			if (cxn->expire && (time((time_t *)NULL) -
			    mbox->msg[msgs].d_time) > cxn->expire) {
				mbox->msg[msgs].flags |= MBF_F_EXPIRE;
				mbox->expire++;
			}
			if (cxn->remove && (time((time_t *)NULL) -
			    mbox->msg[msgs].d_time) > cxn->remove) {
				mbox->msg[msgs].flags |= MBF_F_REMOVE;
				mbox->remove++;
			}
			headerlen = strlen(line);
			mbox->msg[msgs].header = xmalloc(headerlen + 1);
			strcpy(mbox->msg[msgs].header, line);
			if (msgs > 0) {
				mbox->msg[msgs - 1].bytes =
				    (size_t)mbox->msg[msgs].offset -
				    (size_t)mbox->msg[msgs - 1].offset;
				uidlen = snprintf(uidldat, MAXBUFLEN,
				    "%s%s%s%d", mbox->msg[msgs - 1].flags &
				    (MBF_F_EXPIRE | MBF_F_REMOVE) &&
				    cxn->flags & MAILBOX_F_FALSEUIDL ?
				    "1" : "0", cxn->mailpath,
				    mbox->msg[msgs - 1].header,
				    mbox->msg[msgs - 1].bytes);
				free(mbox->msg[msgs - 1].header);
				mbox->msg[msgs - 1].header = NULL;
				MD5Init(&context);
				MD5Update(&context,
				    (unsigned char *)uidldat, (size_t)uidlen);
				MD5Final(digest, &context);
				strcpy(mbox->msg[msgs - 1].uidl, binhex(digest,
				    sizeof(digest)));
			}
		}
		lastblank = FALSE;
	}
	if (msgs > -1) {
		mbox->msg[msgs].bytes = size - (size_t)mbox->msg[msgs].offset;
		uidlen = snprintf(uidldat, MAXBUFLEN, "%s%s%s%d",
		    mbox->msg[msgs].flags & (MBF_F_EXPIRE | MBF_F_REMOVE) &&
		    cxn->flags & MAILBOX_F_FALSEUIDL ? "1" : "0", cxn->mailpath,
		    mbox->msg[msgs].header,
		    mbox->msg[msgs].bytes);
		free(mbox->msg[msgs].header);
		mbox->msg[msgs].header = NULL;
		MD5Init(&context);
		MD5Update(&context, (unsigned char *)uidldat, (size_t)uidlen);
		MD5Final(digest, &context);
		strcpy(mbox->msg[msgs].uidl, binhex(digest, sizeof(digest)));
        }
	mbox->count = msgs;
	mbox->bytes = size;
	if (mbox->count > 0)
		/* Should really use mergesort() here because it is stable
		 * however not everyone has mergesort().
		 */
		qsort((void *)mbox->msg, mbox->count + 1,
			sizeof(struct message), &compar);
	return(TRUE);
}

static int
compar(const void *a, const void *b)
{
	if (flags & MAILBOX_F_SORT_TIME)
		return(((struct message *)a)->offset -
		    ((struct message *)b)->offset);
	else if (flags & MAILBOX_F_SORT_SIZE)
		return(((struct message *)a)->bytes -
		    ((struct message *)b)->bytes);
	else
		/* effectively don't sort */
		return(0);
}

void
mbf_close(struct mbox *mbox, struct connection *cxn)
{
	int     i, ret, del, exp, rem, errors, facility;
	off_t	src, dst;
	size_t	left, bytes, len;
	char	buffer[MAXBUFLEN];

	facility = LOG_INFO;
	flags = (flags & ~MAILBOX_F_SORTMASK) | MAILBOX_F_SORT_TIME;
	qsort((void *)mbox->msg, mbox->count + 1,
	sizeof(struct message), &compar);
	ret = del = exp = rem = errors = 0;
	dst = -1;
	for (i = 0; i <= mbox->count; i++) {
		ret += (mbox->msg[i].flags & MBF_F_READ) > 1;
		if (mbox->msg[i].flags & MBF_F_DELETE) {
			del++;
			mbox->bytes -= mbox->msg[i].bytes;
			if (dst < 0)
				dst = mbox->msg[i].offset;
		}
		else if (cxn->flags & MAILBOX_F_AUTODELETE &&
		    mbox->msg[i].flags & MBF_F_EXPIRE &&
		    mbox->msg[i].flags & MBF_F_READ) {
			exp++;
			mbox->bytes -= mbox->msg[i].bytes;
			if (dst < 0)
				dst = mbox->msg[i].offset;
		}
		else if (mbox->msg[i].flags & MBF_F_REMOVE) {
			rem++;
			mbox->bytes -= mbox->msg[i].bytes;
			if (dst < 0)
				dst = mbox->msg[i].offset;
		}
		else {
			if (dst < 0)
				continue;
			bytes = 0;
			src = mbox->msg[i].offset;
			for (; i <= mbox->count; i++) {
				if ((mbox->msg[i].flags & MBF_F_DELETE) ||
				    (cxn->flags & MAILBOX_F_AUTODELETE && 
				     mbox->msg[i].flags & MBF_F_EXPIRE &&
				     mbox->msg[i].flags & MBF_F_READ) ||
				    (mbox->msg[i].flags & MBF_F_REMOVE)) {
					i--;
					break;
				}
				bytes += mbox->msg[i].bytes;
			}
			left = bytes;
			if (left > MAXBUFLEN)
				left = MAXBUFLEN;
			lseek(mbox->fd, src, SEEK_SET);
			while ((len =
			    read(mbox->fd, buffer, left)) > 0) {
				lseek(mbox->fd, dst, SEEK_SET);
				write(mbox->fd, buffer, len);
				src += len;
				dst += len;
				lseek(mbox->fd, src, SEEK_SET);
				bytes -= len;
				left = bytes;
				if (left > MAXBUFLEN)
					left = MAXBUFLEN;
			}
		}
	}
	free(mbox->msg);

	ftruncate(mbox->fd, (off_t)mbox->bytes);
	close(mbox->fd);
	log_stats(cxn->auth_string, ret, mbox->count + 1 - del - exp - rem,
	    mbox->bytes, errors, del, mbox->expire, exp, mbox->remove, rem);
}

static int
mbf_is_message(struct mbox *mbox, int number)
{
	if (number > mbox->count || number < 0) {
		message(NOSUCH);
		return(FALSE);
	}
	if (mbox->msg[number].flags & MBF_F_DELETE) {
		message(ALREADYDELETED);
		return(FALSE);
	}
	return(TRUE);
}

int
mbf_get_message_lines(struct mbox *mbox, int number, int lines)
{
	char	buffer[MAXBUFLEN], *p;
	int	inbody, count, len;
	char	*line, *offset;
	size_t	bytes, buffleft;

	p = buffer;
	inbody = FALSE;
	count = 0;
	bytes = mbox->msg[number].bytes;

	lseek(mbox->fd, mbox->msg[number].offset, SEEK_SET);
	sendline(SEND_BUF, "+OK sending message ending with a '.' on "
	    "a line by itself");
	memset((void *)buffer, NULL, MAXBUFLEN);
	for (;;) {
		line = p;
		if ((p = strchr(line, '\n')) == NULL) {
			if (bytes == 0 || (inbody && lines > -1 && 
			    count > lines)) {
				break;
			}
			strcpy(buffer, line);
			line = buffer;
			offset = strchr(buffer, '\0');
			buffleft = MAXBUFLEN - (offset - buffer) - 1;
			if (buffleft > bytes)
				buffleft = bytes;
			len = read(mbox->fd, offset, buffleft);
			bytes -= len;
			offset[len] = '\0';
			p = strchr(buffer, '\n');
		}
		*p++ = '\0';
		if (line[0] == '.' && line[1] == '\0') {
			sendline(SEND_BUF, "..");
		}
		else {
			sendline(SEND_BUF, "%s", line);
		}
		if (inbody && lines > -1 && count > lines) {
			break;
		}
		if (!inbody && *p == '\n')
			inbody = TRUE;
		if (inbody == TRUE)
			count++;
	}
	sendline(SEND_FLUSH, ".");
	if (lines == -1) 
		mbox->msg[number].flags |= MBF_F_READ;
	else
		mbox->msg[number].flags |= MBF_F_TOP;
	return(TRUE);
}

static void
moremessages(struct mbox *mbox)
{
        mbox->msg = xrealloc(mbox->msg,
	    (mbox->max += MAXINCR) * sizeof(struct message));
}

static int
mbf_get_uidl_all(struct mbox *mbox)
{
	int	i;

	sendline(SEND_BUF, "+OK sending list ending with a '.' on "
	    "a line by itself");
	for (i = 0; i <= mbox->count; i++) {
		if (mbox->msg[i].flags & MBF_F_DELETE)
			continue;
		sendline(SEND_BUF, "%d %s", i + 1, mbox->msg[i].uidl);
	}
	return(TRUE);
}

static int
mbf_list_all(struct mbox *mbox)
{
	int	i;

	sendline(SEND_BUF, "+OK sending list ending with a '.' on "
	    "a line by itself");
	for (i = 0; i <= mbox->count; i++) {
		if (mbox->msg[i].flags & MBF_F_DELETE)
			continue;
		sendline(SEND_BUF, "%d %d", i + 1, mbox->msg[i].bytes);
	}
	return(TRUE);
}

int
mbf_mbox_op(struct connection *cxn, enum cmd cmd, ...)
{
	static struct mbox		mbox;
	va_list				ap;
	int				arg1, arg2, i, bytes, msgs;
	static int			last = 0;

	switch (cmd) {
	    case SESSION_START:
		flags = cxn->flags;
		if (mbf_open(&mbox, cxn)) {
			bulletin_mbox_op(cxn, SESSION_START);
			sendline(SEND_FLUSH, "+OK %d message%s (%d octets) "
			    "(Expire on RETR: %d Remove on QUIT: %d)",
			    bulletin_mbox_op(cxn, BUL_MSGS) + mbox.count + 1,
			    bulletin_mbox_op(cxn, BUL_MSGS) + mbox.count + 1 ==
			        1 ? "" : "s",
			    mbox.bytes + bulletin_mbox_op(cxn, BUL_SIZE),
			    mbox.expire, mbox.remove);
			return(TRUE);
		}
		return(FALSE);
	    case SESSION_END:
		mbf_close(&mbox, cxn);
		return(TRUE);
	    case DELE:
		va_start(ap, cmd);
		arg1 = va_arg(ap, int);
		va_end(ap);
		if (arg1 - 1 > mbox.count) {
			bulletin_mbox_op(cxn, DELE, mbox.count + 1, arg1);
			break;
		}
		if (!mbf_is_message(&mbox, arg1 - 1))
			return(FALSE);
		mbox.msg[arg1 - 1].flags |= MBF_F_DELETE;
		sendline(SEND_FLUSH, "+OK message deleted");
		break;
	    case LIST:
		va_start(ap, cmd);
		arg1 = va_arg(ap, int);
		va_end(ap);
		if (arg1 >= 0) {
			if (arg1 - 1 > mbox.count) {
				bulletin_mbox_op(cxn, LIST, mbox.count + 1,
				    arg1);
				break;
			}
			if (!mbf_is_message(&mbox, arg1 - 1))
				return(FALSE);
			sendline(SEND_FLUSH, "+OK %d %d", arg1,
			    mbox.msg[arg1 - 1].bytes);
		}
		else {
			mbf_list_all(&mbox);
			bulletin_mbox_op(cxn, LIST, mbox.count + 1, arg1);
			sendline(SEND_FLUSH, ".");
		}
		break;
	    case NOOP:
		sendline(SEND_FLUSH, "+OK");
		break;
	    case QUIT:
		sendline(SEND_FLUSH, "+OK");
		poputil_end();
		mbf_mbox_op(cxn, SESSION_END);
		bulletin_mbox_op(cxn, SESSION_END);
		exit(EX_OK);
		break;
	    case RETR:
		va_start(ap, cmd);
		arg1 = va_arg(ap, int);
		va_end(ap);
		if (arg1 - 1 > mbox.count) {
			bulletin_mbox_op(cxn, RETR, mbox.count + 1, arg1);
			break;
		}
		if (!mbf_is_message(&mbox, arg1 - 1))
			return(FALSE);
		if (last < arg1)
			last = arg1;
		mbf_get_message_lines(&mbox, arg1 - 1, -1);
		break;
	    case RSET:
		for (i = 0; i <= mbox.count; i++)
			mbox.msg[i].flags &= !(MBF_F_DELETE | MBF_F_READ);
		last = 0;
		bulletin_mbox_op(cxn, RSET);
		sendline(SEND_FLUSH, "+OK");
		break;
	    case STAT:
		bytes = msgs = 0;
		for (i = 0; i <= mbox.count; i++) {
			if (mbox.msg[i].flags & MBF_F_DELETE)
				continue;
			bytes += mbox.msg[i].bytes;
			msgs++;
		}
		msgs += bulletin_mbox_op(cxn, BUL_MSGS);
		bytes += bulletin_mbox_op(cxn, BUL_SIZE);
		sendline(SEND_FLUSH, "+OK %d %d", msgs, bytes);
		break;
	    case TOP:
		va_start(ap, cmd);
		arg1 = va_arg(ap, int);
		arg2 = va_arg(ap, int);
		va_end(ap);
		if (arg1 - 1 > mbox.count) {
			bulletin_mbox_op(cxn, TOP, mbox.count + 1, arg1, arg2);
			break;
		}
		if (!mbf_is_message(&mbox, arg1 - 1))
			return(FALSE);
		mbf_get_message_lines(&mbox, arg1 - 1, arg2);
		break;
	    case UIDL:
		va_start(ap, cmd);
		arg1 = va_arg(ap, int);
		va_end(ap);
		if (arg1 >= 0) {
			if (arg1 - 1 > mbox.count) {
				bulletin_mbox_op(cxn, UIDL, mbox.count + 1,
				    arg1);
				break;
			}
			if (!mbf_is_message(&mbox, arg1 - 1))
				return(FALSE);
			sendline(SEND_FLUSH, "+OK %d %s", arg1,
			    mbox.msg[arg1 - 1].uidl);
		}
		else {
			mbf_get_uidl_all(&mbox);
			bulletin_mbox_op(cxn, UIDL, mbox.count + 1, arg1);
			sendline(SEND_FLUSH, ".");
		}
		break;
	    case LAST:	/* backwards compatibility with RFC1460 */
		sendline(SEND_FLUSH, "+OK %d", last);
		break;
	    case BUL_MSGS:		/* not reached */
	    case BUL_SIZE:
	    case APOP:
	    case AUTH:
	    case PASS:
	    case USER:
	    case TIMEDOUT:
	    case INVALCMD:
		break;
	}
	return(FALSE);
}


syntax highlighted by Code2HTML, v. 0.9.1