/*
 * 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_maildir.c,v 1.32 2003/01/24 09:35: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"

extern int	fd_in, fd_out;

/*
 * MailDir Format
 */

#define MDIR_F_OK        0x0001
#define MDIR_F_NOSUCH    0x0002
#define MDIR_F_DELETE    0x0004
#define MDIR_F_EXPIRE    0x0008
#define MDIR_F_REMOVE    0x0010
#define MDIR_F_READ      0x0020
#define MDIR_F_TOP       0x0040
#define MDIR_F_STAT      0x0080
#define MDIR_F_NEW       0x0100

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

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

struct mbox {
	int		fd;
	char		*new_dir;
	char		*cur_dir;
	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 *);
static int	maildir_open(struct mbox *, struct connection *);
static int	maildir_is_message(struct mbox *, int);
static int	maildir_list_all(struct mbox *);
static int	maildir_get_uidl_all(struct mbox *);
static int	maildir_get_message_lines(struct mbox *, int, int);
static void	maildir_close(struct mbox *, struct connection *);

static int
maildir_open(struct mbox *mbox, struct connection *cxn)
{
	DIR		*dirp;
	struct dirent	*dp;
	struct stat	stat_s;
	char		*dir, uidldat[MAXBUFLEN + 1];
	unsigned char	digest[16];
	int		pathlen, uidlen, n, dirs, exists = FALSE;
	MD5_CTX		context;

	memset(mbox, '\0', sizeof(struct mbox));
	mbox->count = -1;
	mbox->max = -1;

	if (cxn->flags & MAILBOX_F_MAILDIRCOMPAT) {
		dirs = 0;
		mbox->new_dir = xmalloc(strlen(cxn->mailpath) + 2);
		strcpy(mbox->new_dir, cxn->mailpath);
		strcat(mbox->new_dir, "/");
	}
	else {
		dirs = 1;
		mbox->new_dir = xmalloc(strlen(cxn->mailpath) + 5);
		strcpy(mbox->new_dir, cxn->mailpath);
		strcat(mbox->new_dir, "/new/");
		mbox->cur_dir = xmalloc(strlen(cxn->mailpath) + 5);
		strcpy(mbox->cur_dir, cxn->mailpath);
		strcat(mbox->cur_dir, "/cur/");
	}

	for (n = 0; n <= dirs; n++) {
		if (n == 0) {
			dir = mbox->new_dir;
		}	
		else {
			dir = mbox->cur_dir;
		}
		pathlen = strlen(dir);

		if ((dirp = opendir(dir))) {
			exists = TRUE;
			while ((dp = readdir(dirp)) != NULL) {
				if (!strncmp(dp->d_name, ".", 1) ||
				    !strncmp(dp->d_name, "..", 2))
					continue;
				if (mbox->count + 1 >= mbox->max)
					moremessages(mbox);
				mbox->count++;
				mbox->msg[mbox->count].flags = FALSE;
				mbox->msg[mbox->count].directory = dir;
				mbox->msg[mbox->count].filename =
				    xmalloc(strlen(dp->d_name) + 1);
				strcpy(mbox->msg[mbox->count].filename,
				    dp->d_name);
				mbox->msg[mbox->count].path =
				    xmalloc(pathlen + strlen(dp->d_name) + 1);
				strcpy(mbox->msg[mbox->count].path, dir);
				strcat(mbox->msg[mbox->count].path, dp->d_name);
				stat(mbox->msg[mbox->count].path, &stat_s);
				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;
				if (cxn->expire && (time((time_t *)NULL) -
				    stat_s.st_mtime) > cxn->expire) {
					mbox->msg[mbox->count].flags |=
					    MDIR_F_EXPIRE;
					mbox->expire++;
				}
				if (cxn->remove && (time((time_t *)NULL) -
				    stat_s.st_mtime) > cxn->remove) {
					mbox->msg[mbox->count].flags |=
					    MDIR_F_REMOVE;
					mbox->remove++;
				}
				if (n == 0)
					mbox->msg[mbox->count].flags |=
					    MDIR_F_NEW;
				uidlen = snprintf(uidldat, MAXBUFLEN,
				    "%d%s%d", mbox->msg[mbox->count].flags &
				    (MDIR_F_EXPIRE | MDIR_F_REMOVE) &&
				    cxn->flags & MAILBOX_F_FALSEUIDL ?
				    time(NULL) : mbox->msg[mbox->count].d_time,
				    dp->d_name,
				    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 (exists) {
		if (mbox->count >= 0)
			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)->d_time -
		    ((struct message *)b)->d_time);
	else if (flags & MAILBOX_F_SORT_SIZE)
		return(((struct message *)a)->bytes -
		    ((struct message *)b)->bytes);
	else
		return(0);

}

static void
maildir_close(struct mbox *mbox, struct connection *cxn)
{
	int	i, ret, del, exp, rem, errors;
	char	*path = NULL;

	ret = del = exp = rem = errors = 0;

	for (i = 0; i <= mbox->count; i++) {
		ret += (mbox->msg[i].flags & MDIR_F_READ) / MDIR_F_READ;
		if (mbox->msg[i].flags & MDIR_F_DELETE) {
			if (unlink(mbox->msg[i].path) != NULL) {
				syslog(LOG_NOTICE, "Problem deleting %s "
					"(error: %s)",
					mbox->msg[i].path, strerror(errno));
				errors++;
				free(mbox->msg[i].path);
				continue;
			}
			del++;
			mbox->bytes -= mbox->msg[i].bytes;
			free(mbox->msg[i].path);
			continue;
		}
		else if (cxn->flags & MAILBOX_F_AUTODELETE &&
		    mbox->msg[i].flags & MDIR_F_EXPIRE &&
		    mbox->msg[i].flags & MDIR_F_READ) {
			if (unlink(mbox->msg[i].path) != NULL) {
				syslog(LOG_NOTICE, "Problem expiring %s "
					"(error: %s)",
					mbox->msg[i].path, strerror(errno));
				errors++;
				free(mbox->msg[i].path);
				continue;
			}
			exp++;
			mbox->bytes -= mbox->msg[i].bytes;
			free(mbox->msg[i].path);
			continue;
		}
		else if (mbox->msg[i].flags & MDIR_F_REMOVE) {
			if (unlink(mbox->msg[i].path) != NULL) {
				syslog(LOG_NOTICE, "Problem removing %s "
					"(error: %s)",
					mbox->msg[i].path, strerror(errno));
				errors++;
				free(mbox->msg[i].path);
				continue;
			}
			rem++;
			mbox->bytes -= mbox->msg[i].bytes;
			free(mbox->msg[i].path);
			continue;
		}
		else if (!(cxn->flags & MAILBOX_F_MAILDIRCOMPAT) &&
		    mbox->msg[i].flags & MDIR_F_READ &&
		    mbox->msg[i].flags & MDIR_F_NEW) {
			path = xrealloc(path, strlen(mbox->msg[i].filename) +
			    strlen(mbox->cur_dir) + 1);
			strcpy(path, mbox->cur_dir);
			strcat(path, mbox->msg[i].filename);
			if (link(mbox->msg[i].path, path) == 0)
				unlink(mbox->msg[i].path);
		}
	}
	log_stats(cxn->auth_string, ret, mbox->count + 1 - del - exp - rem,
	    mbox->bytes, errors, del, mbox->expire, exp, mbox->remove, rem);
	free(mbox->msg);
	free(path);
}

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

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

	mbox->fd = openlock(mbox->msg[number].path, O_RDWR|O_NONBLOCK);
	if (mbox->fd < 0) {
		sendline(SEND_FLUSH, "-ERR error opening message: %s",
		    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(mbox->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 |= MDIR_F_READ;
	else
		mbox->msg[number].flags |= MDIR_F_TOP;
	close(mbox->fd);
	return(TRUE);
}

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

static int
maildir_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 & MDIR_F_DELETE)
			continue;
		sendline(SEND_BUF, "%d %s", i + 1, mbox->msg[i].uidl);
	}
	return(TRUE);
}

static int
maildir_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 & MDIR_F_DELETE)
			continue;
		sendline(SEND_BUF, "%d %d", i + 1, mbox->msg[i].bytes);
	}
	return(TRUE);
}

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

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

	switch (cmd) {
	    case SESSION_START:
		flags = cxn->flags;
		if (maildir_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);
		break;
	    case SESSION_END:
		maildir_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 (!maildir_is_message(&mbox, arg1 - 1))
			return(FALSE);
		mbox.msg[arg1 - 1].flags |= MDIR_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 (!maildir_is_message(&mbox, arg1 - 1))
				return(FALSE);
			sendline(SEND_FLUSH, "+OK %d %d", arg1,
			    mbox.msg[arg1 - 1].bytes);
		}
		else {
			maildir_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();
		maildir_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 (last < arg1)
			last = arg1;
		if (!maildir_is_message(&mbox, arg1 - 1))
			return(FALSE);
		if (last < arg1)
			last = arg1;
		maildir_get_message_lines(&mbox, arg1 - 1, -1);
		break;
	    case RSET:
		for (i = 0; i <= mbox.count; i++)
			mbox.msg[i].flags &= !(MDIR_F_DELETE | MDIR_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 & MDIR_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 (!maildir_is_message(&mbox, arg1 - 1))
			return(FALSE);
		maildir_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 (!maildir_is_message(&mbox, arg1 - 1))
				return(FALSE);
			sendline(SEND_FLUSH, "+OK %d %s", arg1,
			    mbox.msg[arg1 - 1].uidl);
		}
		else {
			maildir_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_SIZE:		/* not reached */
	    case BUL_MSGS:
	    case APOP:
	    case AUTH:
	    case PASS:
	    case USER:
	    case TIMEDOUT:
	    case INVALCMD:
		break;
	}
	return(FALSE);
}


syntax highlighted by Code2HTML, v. 0.9.1