/*
 * 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_bulletin.c,v 1.10 2003/01/24 09:35:19 ianf Exp $
 */

#include <sys/types.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"

/*
 * Global Variables
 */

extern struct config	config;

/* Bulletins */

#define BUL_F_OK        0x0001
#define BUL_F_NOSUCH    0x0002
#define BUL_F_DELETE    0x0004
#define BUL_F_EXPIRE    0x0008
#define BUL_F_REMOVE    0x0010
#define BUL_F_READ      0x0020
#define BUL_F_TOP       0x0040
#define BUL_F_STAT      0x0080

struct message {
	char    *path;
	char    uidl[33];
	time_t  d_time;
	size_t  bytes;
	int     flags;
	int	fd;
};

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

static int	have_we_seen(struct connection *cxn, char *dir, char *bulletin);
static void	moremessages(struct mbox *mbox);
static int	bulletin_open(struct mbox *mbox, struct connection *cxn);
static void	bulletin_close(struct mbox *mbox, struct connection *cxn);
static int	bulletin_is_message(struct mbox *mbox, int number);
static int	bulletin_list_all(struct mbox *mbox, int offset);
static int	bulletin_uidl_all(struct mbox *mbox, int offset);
static int	bulletin_get_message_lines(struct connection *cxn,
		    struct mbox *mbox, int number, int lines);

static int
have_we_seen(struct connection *cxn, char *dir, char *bulletin)
{
	DB	*dp;
	DBT	key, data;
	char	*path;

	path = xmalloc(strlen(dir) + strlen(bulletin) + 9);
	strcpy(path, dir);
	strcat(path, "/.db/");
	strcat(path, bulletin);
	strcat(path, ".db");

	seteuid(cxn->uid);
	if (!(dp = dbopen(path, O_RDONLY|O_EXLOCK,
	    S_IRUSR|S_IWUSR, DB_HASH, NULL))) {
		free(path);
		seteuid(cxn->euid);
		return(FALSE);
	}

	key.data = (u_char *)cxn->auth_string;
	key.size = strlen(key.data);

	if (!(dp->get)(dp, &key, &data, 0)) {
		(dp->close)(dp);
		free(path);
		seteuid(cxn->euid);
		return(TRUE);
	}
	(dp->close)(dp);
	free(path);
	seteuid(cxn->euid);
	return(FALSE);
}

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

static int
bulletin_open(struct mbox *mbox, struct connection *cxn)
{
	MD5_CTX		 context;
	DIR		*dirp = NULL;
	struct dirent	*dp;
	struct stat	 stat_s;
	char		*path[2], uidldat[MAXBUFLEN + 1];
	unsigned char	 digest[16];
	int		 pathlen[2], uidlen, i;

	mbox->count = -1;
	mbox->max = -1;
	mbox->bytes = 0;
	mbox->msg = NULL;
	i = 1;

	if (cxn->realm) {
		pathlen[0] = strlen(cxn->bulletinpath) + strlen(cxn->realm) + 2;
		path[0] = xmalloc(pathlen[0]);
		strcpy(path[0], cxn->bulletinpath);
		strcat(path[0], "/");
		strcat(path[0], cxn->realm);
		i = 0;
	}
	path[1] = cxn->bulletinpath;
	pathlen[1] = strlen(path[1]);

	for (;i < 2; i++) {
		if (!(dirp = opendir(path[i]))) {
			if (cxn->realm)
				free(path[0]);
			return(FALSE);
		}
		while ((dp = readdir(dirp)) != NULL) {
			if (have_we_seen(cxn, path[i], dp->d_name))
				continue;
			if (++mbox->count > mbox->max)
				moremessages(mbox);
			mbox->msg[mbox->count].fd = -1;

			mbox->msg[mbox->count].path =
			    xmalloc(strlen(dp->d_name) + pathlen[i] + 3);
			strcpy(mbox->msg[mbox->count].path, path[i]);
			strcat(mbox->msg[mbox->count].path, "/");
			strcat(mbox->msg[mbox->count].path, dp->d_name);
			stat(mbox->msg[mbox->count].path, &stat_s);
			if ((stat_s.st_mode & S_IFMT) == S_IFDIR) {
				free(mbox->msg[mbox->count--].path);
				continue;
			}
			mbox->msg[mbox->count].d_time = stat_s.st_mtime;
			mbox->msg[mbox->count].bytes = stat_s.st_size;
			mbox->bytes += stat_s.st_size;
			uidlen = snprintf(uidldat, MAXBUFLEN, "%d%s%d",
			    mbox->msg[mbox->count].d_time,
			    mbox->msg[mbox->count].path,
			    mbox->msg[mbox->count].bytes);
			MD5Init(&context);
			MD5Update(&context, (unsigned char *)uidldat,
			    (size_t)uidlen);
			MD5Final(digest, &context);
			strcpy(mbox->msg[mbox->count].uidl,
			    binhex(digest, sizeof(digest)));
		}
	}
	closedir(dirp);
	if (cxn->realm)
		free(path[0]);
	return(TRUE);
}

static void
bulletin_close(struct mbox *mbox, struct connection *cxn)
{
	DB	*dp;
	DBT	key, data;
	char	*path, *p, *q;
	int	i;
	time_t	tm;

	path = NULL;
	for (i = 0; i <= mbox->count; i++) {
		if (!(mbox->msg[i].flags & BUL_F_DELETE))
			continue;
	
		/* Split the path into the directory and filename */
		for (p = strchr(mbox->msg[i].path, '/');
		    p && (q = strchr(p + 1, '/')); p = q);
		if (p == NULL)
			continue;
		*p++ = '\0';
	
		path = xrealloc(path, strlen(mbox->msg[i].path) +
		    strlen(p) + 9);
		strcpy(path, mbox->msg[i].path);
		strcat(path, "/.db/");
		strcat(path, p);
		strcat(path, ".db");

		seteuid(cxn->uid);
		if (!(dp = dbopen(path, O_RDWR|O_CREAT|O_EXLOCK,
		    S_IRUSR|S_IWUSR, DB_HASH, NULL))) {
			free(path);
			seteuid(cxn->euid);
			return;
		}

		key.data = (u_char *)cxn->auth_string;
		key.size = strlen(key.data);

		tm = time(NULL);
		data.data = (u_char *)&tm;
		data.size = sizeof(tm);;

		(dp->put)(dp, &key, &data, 0);
		(dp->close)(dp);
		seteuid(cxn->euid);
	}
	free(path);
}

static int
bulletin_get_message_lines(struct connection *cxn, struct mbox *mbox,
    int number, int lines)
{
	char	buffer[MAXBUFLEN], *p;
	int	fd, inbody, count, bytes, len, buffleft;
	char	*line, *offset;

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

	seteuid(cxn->uid);
	fd = openlock(mbox->msg[number].path, O_RDWR|O_NONBLOCK);
	seteuid(cxn->euid);
	if (fd < 0) {
		sendline(SEND_FLUSH, "-ERR (bulletin) error opening message "
		    "'%s': %s", mbox->msg[number].path, strerror(errno));
		return(FALSE);
	}

	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(fd, offset, buffleft);
			bytes -= len;
			offset[buffleft] = '\0';
			p = strchr(buffer, '\n');
		}
		if (p == NULL) {
			sendline(SEND_BUF, "");
			sendline(SEND_BUF, "-------------------------------------------");
			sendline(SEND_BUF, "The POP server encountered a line way in");
			sendline(SEND_BUF, "excess of 988 characters at this point in");
			sendline(SEND_BUF, "the message '%s'.", mbox->msg[number].path);
			sendline(SEND_BUF, "This is in violation of RFC2822 section");
			sendline(SEND_BUF, "2.1.1 .");
			sendline(SEND_BUF, "");
			sendline(SEND_BUF, "This is not the end of the actual message.");
			sendline(SEND_BUF, "Ask the sender to resend the message in a");
			sendline(SEND_BUF, "manner conforming to RFC2822.");
			sendline(SEND_BUF, "-------------------------------------------");
			break;
		}

		*p++ = '\0';
		if (line[0] == '.' && line[1] == '\0') {
			sendline(SEND_BUF, "..");
		}
		else {
			if (!inbody  && !strncmp("From ", line, 5))
				continue;
			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 |= BUL_F_READ;
	else
		mbox->msg[number].flags |= BUL_F_TOP;
	close(fd);
	return(TRUE);
}

static int
bulletin_uidl_all(struct mbox *mbox, int offset)
{
	int	i;

	for (i = 0; i <= mbox->count; i++) {
		if (mbox->msg[i].flags & BUL_F_DELETE)
			continue;
		sendline(SEND_BUF, "%d %s", offset + i + 1, mbox->msg[i].uidl);
	}
	return(TRUE);
}

static int
bulletin_list_all(struct mbox *mbox, int offset)
{
	int	i;

	for (i = 0; i <= mbox->count; i++) {
		if (mbox->msg[i].flags & BUL_F_DELETE)
			continue;
		sendline(SEND_BUF, "%d %d", offset + i + 1, mbox->msg[i].bytes);
	}
	return(TRUE);
}

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

int
bulletin_mbox_op(struct connection *cxn, enum cmd cmd, ...)
{
	static struct mbox		mbox;
	va_list				ap;
	int				arg1, arg2, offset, i;

	if (cxn->bulletinpath == NULL)
		return(0);

	switch (cmd) {
	    case SESSION_START:
		bulletin_open(&mbox, cxn);
		break;
	    case SESSION_END:
		bulletin_close(&mbox, cxn);
		return(TRUE);
	    case DELE:
		va_start(ap, cmd);
		offset = va_arg(ap, int);
		arg1 = va_arg(ap, int);
		va_end(ap);
		if (!bulletin_is_message(&mbox, arg1 - offset - 1))
			return(FALSE);
		mbox.msg[arg1 - offset - 1].flags |= BUL_F_DELETE;
		sendline(SEND_FLUSH, "+OK message deleted");
		break;
	    case LIST:
		va_start(ap, cmd);
		offset = va_arg(ap, int);
		arg1 = va_arg(ap, int);
		va_end(ap);
		if (arg1 >= 0) {
			if (!bulletin_is_message(&mbox, arg1 - offset - 1))
				return(FALSE);
			sendline(SEND_FLUSH, "+OK %d %d", arg1,
			    mbox.msg[arg1 - offset - 1].bytes);
		}
		else {
			bulletin_list_all(&mbox, offset);
		}
		break;
	    case QUIT:
		bulletin_mbox_op(cxn, SESSION_END);
		break;
	    case RETR:
		va_start(ap, cmd);
		offset = va_arg(ap, int);
		arg1 = va_arg(ap, int);
		va_end(ap);
		if (!bulletin_is_message(&mbox, arg1 - offset - 1))
			return(FALSE);
		bulletin_get_message_lines(cxn, &mbox, arg1 - offset - 1, -1);
		break;
	    case RSET:
		for (i = 0; i <= mbox.count; i++)
			mbox.msg[i].flags &= !(BUL_F_DELETE | BUL_F_READ);
		break;
	    case TOP:
		va_start(ap, cmd);
		offset = va_arg(ap, int);
		arg1 = va_arg(ap, int);
		arg2 = va_arg(ap, int);
		va_end(ap);
		if (!bulletin_is_message(&mbox, arg1 - offset - 1))
			return(FALSE);
		bulletin_get_message_lines(cxn, &mbox, arg1 - offset - 1, arg2);
		break;
	    case UIDL:
		va_start(ap, cmd);
		offset = va_arg(ap, int);
		arg1 = va_arg(ap, int);
		va_end(ap);
		if (arg1 >= 0) {
			if (!bulletin_is_message(&mbox, arg1 - offset - 1))
				return(FALSE);
			sendline(SEND_FLUSH, "+OK %d %s", arg1,
			    mbox.msg[arg1 -offset - 1].uidl);
		}
		else
			bulletin_uidl_all(&mbox, offset);
		break;
	    case BUL_MSGS:
		return(mbox.count + 1);
	    case BUL_SIZE:
		return(mbox.bytes);
	    case NOOP:
		/* noop not required for meta mailbox bulletin */
	    case LAST:
		/* last not required for meta mailbox bulletin */
	    case STAT:
		/* stat not required for meta mailbox bulletin */
	    case APOP:	/* not reached */
	    case AUTH:
	    case PASS:
	    case USER:
	    case INVALCMD:
	    case TIMEDOUT:
		break;
	}
	return(TRUE);
}


syntax highlighted by Code2HTML, v. 0.9.1