/* lsmbox -- list nr of total and read messages for one or several mailboxes
 *
 * Copyright (C) 2002-2006, 2010, 2014 David Weinehall
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2
 *  as published by the Free Software Foundation.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to
 *  the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */
#include "lsmbox.h"

extern int optind;
extern char *optarg;

static char buf[BUF_SIZE];

static const char *progname;

static char *homepath = NULL;
static char *mailcommand = NULL;
static char *spoolfilepath = NULL;
static char *mailfolderpath = NULL;
static char **mailboxes = NULL;
static int markold = 1;
static int shortnames = 0;
static int continuous = 0;
static int nosummary = 0;
static int selected = -1;
static int execute = -1;
static int newonly = 0;
static int nototal = 0;
static int noold = 0;
static int quit = 0;
static int padding = -1;

static time_t mailcheck = 0;
static time_t selectedtimer = 0;

static char *unseenstr = NULL;
static char *bugfile = NULL;

struct mailstat {
	char *name;
	int exists;
	mbox_type_t type;
	unsigned long total;
	unsigned long new;
	unsigned long old;
	off_t size;
	time_t mtime;
	time_t atime;
} mailstat;

static struct mailstat *mboxes = NULL;

#if NCURSES_MOUSE_VERSION == 1
static mmask_t oldmmask;
static MEVENT mouseevent;
#endif /* NCURSES_MOUSE_VERSION == 1 */

/**
 * Show usage-information for the program
 */
static void usage(void)
{
	fprintf(stdout, "%s%s%s%s%s",
		_("Usage: "), progname,
		_(" [OPTION]... [MAILBOX]...\n"
		  "List the number of total, old unread, and new messages\n"
		  "for the specified mailbox(es).\n"
		  "\n"),
#ifdef USE_CURSES
		_("  -c, --continuous      update statistics continuously\n"
		  "  -i, --check-interval  interval between mailchecks\n"),
#else
		"",
#endif /* USE_CURSES */
		_("  -n, --new-only        only display mailboxes with new "
		  "messages\n"
		  "  -o, --no-old          do not display the number of old "
		  "unread messages\n"
		  "                          each mailbox contains\n"
		  "  -p, --padding         padding for the mailbox column;\n"
		  "                          leave empty to adjust to "
		  "smallest possible\n"
		  "  -s, --no-summary      do not display a summary\n"
		  "  -S, --short-names     only show the name of the mailbox\n"
		  "                          rather than the entire path\n"
		  "  -t, --no-total        do not display the total number of "
		  "messages\n"
		  "                          each mailbox contains\n"
		  "      --help            display this help and exit\n"
		  "      --version         output version information and "
		  "exit\n"
		  "\n"
		  "Report bugs to <tao@acc.umu.se>.\n"));
}

/**
 * Show version-information for the program
 */
static void version(void)
{
	fprintf(stdout, _("%s v%s\n%s"),
		progname,
		PACKAGE_VERSION,
		_("Written by David Weinehall.\n"
		  "\n"
		  "Copyright (C) 2002-2007, 2010, 2014 David Weinehall\n"
		  "This is free software; see the source for copying "
		  "conditions.  There is NO\n"
		  "warranty; not even for MERCHANTABILITY or FITNESS FOR "
		  "A PARTICULAR PURPOSE.\n"));
}

/**
 * Reset curses
 */
static void reset_curses(void)
{
#ifdef USE_CURSES
	endwin();
#else
	;
#endif /* USE_CURSES */
}

/**
 * Setup curses
 *
 * @return 1 on success, 0 on failure
 */
static int init_curses(void)
{
#ifdef USE_CURSES
	initscr();
	assert(!atexit(reset_curses));
	assert(start_color() == OK);

	assert(init_pair(1, COLOR_CYAN, COLOR_BLACK) == OK);
	assert(init_pair(2, COLOR_WHITE, COLOR_BLUE) == OK);
	assert(init_pair(3, COLOR_CYAN, COLOR_BLUE) == OK);
	attron(COLOR_PAIR(0));

	curs_set(0);
	noecho();
	keypad(stdscr, TRUE);

	if (nodelay(stdscr, 1) == ERR)
		return 0;

	assert(refresh() != ERR);
	/* Since refresh() didn't return an error we'll clear errno,
	 * just in case, since weird errnos returned by refresh() has
	 * been observed
	 */
	errno = 0;

#if NCURSES_MOUSE_VERSION == 1
	mousemask(BUTTON1_CLICKED, &oldmmask);
#endif /* NCURSES_MOUSE_VERSION == 1 */
#endif /* USE_CURSES */

	return 1;
}

/**
 * Setup locales
 *
 * @return 0 on success, errno on failure
 */
static int init_locales(const char *name)
{
	int status = 0;

#if ENABLE_NLS
	setlocale(LC_ALL, "");

	if (!bindtextdomain(PACKAGE_NAME, LOCALEDIR) && errno == ENOMEM) {
		status = errno;
		goto EXIT;
	}

	if (!textdomain(PACKAGE_NAME) && errno == ENOMEM) {
		status = errno;
		return 0;
	}

EXIT:
	/* In this error-message we don't use _(), since we don't
	 * know where the locales failed, and we probably won't
	 * get a reasonable result if we try to use them.
	 */
	if (status) {
		fprintf(stderr,
			"%s: `%s' failed; %s. Aborting.\n",
			name, "init_locales", strerror(errno));
	} else {
		errno = 0;
	}
#endif /* ENABLE_NLS */

	progname = name;

	return status;
}

/**
 * Update screen
 */
static void update_screen(void)
{
#ifdef USE_CURSES
	if (continuous) {
		clrtobot();
		assert(refresh() != ERR);
		/* Since refresh() didn't return an error we'll clear errno,
		 * just in case, since weird errnos returned by refresh() has
		 * been observed
		 */
		errno = 0;
		assert(move(0, 0) != ERR);
	}
#else
	;
#endif /* USE_CURSES */
}

/**
 * Enable bold text-attribute
 */
static void bold_on(void)
{
#ifdef USE_CURSES
	if (continuous)
		attron(A_BOLD);
#else
	;
#endif /* USE_CURSES */
}

/**
 * Disable bold text-attribute
 */
static void bold_off(void)
{
#ifdef USE_CURSES
	if (continuous)
		attroff(A_BOLD);
#else
	;
#endif /* USE_CURSES */
}

/**
 * Enable text hilighting
 */
static void hilight_on(int hilight, int selected_line)
{
#ifdef USE_CURSES
	if (continuous) {
		attrset(COLOR_PAIR((hilight ? 1 : 0) +
			(selected_line ? 2 : 0)));

		if (hilight && selected_line)
			bold_on();
	}
#else
	(void)selected_line;
#endif /* USE_CURSES */
}

/**
 * Disable text hilighting
 */
static void hilight_off(void)
{
#ifdef USE_CURSES
	if (continuous) {
		attrset(COLOR_PAIR(0));
		bold_off();
	}
#else
	;
#endif /* USE_CURSES */
}

/**
 * Wrapper for getch()
 *
 *  @return the keypress or 0 if no key was pressed
 */
static int curses_getch(void)
{
#ifdef USE_CURSES
	int c = 0;

	if (continuous)
		c = wgetch(stdscr);

	if (c == ERR && errno == EINTR)
		errno = 0;

	return c == ERR ? 0 : c;
#else
	return 0;
#endif /* USE_CURSES */
}

/**
 * Local version of basename
 *
 * @param str A pointer to the basename part of the string
 * @return basename of string
 */
char *basenamep(const char *const str)
{
	char *tmp = strrchr(str, '/');

	if (!tmp)
		tmp = (char *)str;
	else
		tmp++;

	return tmp;
}

/**
 * Move to previous existing mailbox
 */
static void previous_mbox(void)
{
	while (selected > -1) {
		if (mboxes[--selected].exists)
			break;
	}
}

/**
 * Move to next existing mailbox
 */
static void next_mbox(void)
{
	while (selected == -1 || mboxes[selected].name)
		if (mboxes[++selected].exists)
			break;
}

/**
 * Check for keypresses and mouse-events
 *
 * @return 1 on event, 0 otherwise
 */
static int eventcheck(void)
{
	int retval = 0;

#ifdef USE_CURSES
	if (continuous) {
		retval = 1;

		switch (curses_getch()) {
		case 'q':
			quit = 1;
			break;

		case KEY_UP:
			previous_mbox();
			selectedtimer = 0;
			break;

		case KEY_DOWN:
			next_mbox();
			selectedtimer = 0;
			break;

		case '\n':
			execute = selected;
			break;

#if NCURSES_MOUSE_VERSION == 1
		case KEY_MOUSE:
			getmouse(&mouseevent);
			break;
#endif /* NCURSES_MOUSE_VERSION == 1 */

		default:
			retval = 0;
			break;
		}
	}

#endif /* USE_CURSES */
	return retval;
}

/**
 * Output a formatted string
 *
 * @param fmt The formatting string
 * @param args The rest of the arguments
 */
static void output_string(const char *fmt, ...)
{
	va_list args;

	va_start(args, fmt);
#ifdef USE_CURSES
	if (continuous)
		vw_printw(stdscr, (char *)fmt, args);
	else
		vfprintf(stdout, fmt, args);
#else
	vfprintf(stdout, fmt, args);
#endif /* USE_CURSES */
	va_end(args);
}

/**
 * Calculate padsize
 *
 * @param str The string to output
 * @param maxlen The maxlen of the line
 */
void output_string_padded(const char *const str, int maxlen)
{
	char *fmt;

	assert(asprintf(&fmt, "%%-%ds  ", maxlen));
	output_string(fmt, str);
	free(fmt);
}

/**
 * Wrapper for strdup that checks for NULL
 *
 * @param str A pointer to the string to duplicate
 * @return NULL if str is NULL, the duplicated string otherwise
 */
static char *strdup_null(const char *str)
{
	return str ? strdup(str) : NULL;
}

/**
 * Copy a string and return a pointer to its end
 *
 * @param dst A pointer to the destination
 * @param src A pointer to the source
 * @return A pointer to the end of the destination string
 */
static char *internal_stpcpy(char *dst, const char *src)
{
	if (!dst || !src)
		return NULL;

	while ((*dst++ = *src++) != '\0')
		/* nothing */;

	return dst - 1;
}

/**
 * Join a variable amount of strings,
 * with an optional delimiter between each
 *
 * @param delimiter The string delimiter
 * @param ... The strings to join
 * @return A newly allocated string with the result
 */
static char *strjoin(const char *delimiter, ...)
{
	va_list args;
	size_t dlen;
	size_t len;
	char *str;
	char *tmp;
	char *ptr;

	if (delimiter == NULL)
		delimiter = "";

	dlen = strlen(delimiter);

	va_start(args, delimiter);

	if ((tmp = va_arg(args, char *))) {
		len = strlen(tmp) + 1;

		while ((tmp = va_arg(args, char *)))
			len += dlen + strlen(tmp);

		va_end(args);

		assert(str = calloc(len, sizeof (char)));

		va_start(args, delimiter);

		tmp = va_arg(args, char *);
		ptr = internal_stpcpy(str, tmp);

		while ((tmp = va_arg(args, char *))) {
			ptr = internal_stpcpy(ptr, delimiter);
			ptr = internal_stpcpy(ptr, tmp);
		}
	} else {
		str = strdup("");
	}

	va_end(args);

	return str;
}

/**
 * Split a string into an array, using the specified delimiter
 * to tell where to make the splits
 *
 * @param str A pointer to the string to split
 * @param delimiter A pointer to the delimiter
 * @param max_tokens The maximum number of tokens to split the string into
 *                   into; 0 means that the string should be fully split
 * @return A NULL-terminated, newly allocated array of strings
 */
static char **strsplit(const char *str, const char *delimiter,
		       const int max_tokens)
{
	int asize = 1;
	size_t dlen;
	const char *tmp2;
	char **array = NULL;
	char *tmp;

	if (!str || !delimiter || !delimiter[0])
		return NULL;

	dlen = strlen(delimiter);

	tmp2 = str;

	while ((tmp = strstr(tmp2, delimiter)) &&
	       (!max_tokens || asize <= max_tokens)) {
		size_t len;
		char *new;

		len = tmp - tmp2;
		assert(new = calloc(len + 1, sizeof (char)));
		strncpy(new, tmp2, len);
		new[len] = '\0';

		asize++;
		assert(array = realloc(array, sizeof (char *) * asize));
		array[asize - 2] = new;

		tmp2 = tmp + dlen;
	}

	/* add the remaining string */
	asize++;
	assert(array = realloc(array, sizeof (char *) * asize));
	array[asize - 2] = strdup_null(tmp2);

	/* NULL terminate the array */
	array[asize - 1] = NULL;

	return array;
}

/**
 * Compare two unsigned long values, and
 * -1 if num1 < num2,
 *  0 if num1 == num2,
 *  1 if num1 > num2
 *
 * @param num1 A pointer to the first number
 * @param num2 A pointer to the second number
 * @return -1 if num1 < num2
 *          0 if num1 == num2
 *          1 if num1 > num2
 */
static int ulongcmp(const void *num1, const void *num2)
{
	unsigned long long1 = *(const unsigned long *)num1;
	unsigned long long2 = *(const unsigned long *)num2;

	if (long1 < long2)
		return -1;
	else
		return (long1 > long2);
}

/**
 * Skip all leading whitespace in a string
 *
 * @param str The string to process
 * @return A new pointer into the string with leading whitespace skipped
 */
static char *skip_whitespace(char *str)
{
	for (; *str != 0; str++)
		if (!isspace((int)*str))
			break;

	return str;
}

/**
 * Chop off all trailing whitespace in a string
 *
 * @param str The string to process
 */
static void chop_whitespace(char *str)
{
	char *tmp = strlen(str) + str - 1;

	for (; tmp != str; *tmp-- = '\0')
		if (!isspace((int)*str))
			break;
}

/**
 * Skip past a configuration-option to its value
 *
 * @param str The string to process
 * @param skip SKIP_EQ to skip `='
 *	       SKIP_QU to skip `"' and `''
 * @return A new pointer to the value of the configuration-option
 */
static char *skip_config_option(char *str, int skip)
{
	for (; *str != 0; str++)
		if (isspace((int)*str) ||
		    ((skip & SKIP_EQ) && *str == '=') ||
		    ((skip & SKIP_QU) && (*str == '"' || *str == '\'')))
			break;

	for (; *str != 0; str++)
		if (!isspace((int)*str) &&
		    (!(skip & SKIP_EQ) || *str != '=') &&
		    (!(skip & SKIP_QU) || (*str != '"' && *str != '\'')))
			break;

	return str;
}

/**
 * Get the user-name of the caller of the program
 *
 * @return A pointer to the user-name
 */
static const char *get_user_name(void)
{
	struct passwd *pw;

	assert((pw = getpwuid(getuid())));

	return pw->pw_name;
}

/**
 * Free a NULL-terminated array of strings, and the array itself
 *
 * @param array The array to free
 */
static void strfreev(char **array)
{
	int i;

	if (!array)
		return;

	for (i = 0; array[i]; i++)
		free(array[i]);

	free(array);
}

/**
 * Expand a path-name
 *
 * @param str The path to expand
 * @return A pointer to the string with the new path;
 *         needs to be freed after use
 */
static char *path_expand(const char *str)
{
	char *tmp = NULL, *tmp2 = NULL;
	struct passwd *pw;

	if (!strcmp(str, "!"))
		tmp = strdup_null(spoolfilepath);
	else if (*str == '+' || *str == '=')
		tmp = strjoin("/", mailfolderpath, str + 1, NULL);
	else if (strchr(str, '/'))
		tmp = strdup(str);
	else
		tmp = strjoin("/", mailfolderpath, str, NULL);

	if (*tmp == '~') {
		char *rest = NULL;

		if (*(tmp + 1) == '/') {
			assert((pw = getpwuid(getuid())));
		} else {
			char *name = strdup_null(tmp + 1);
			char *end;
			end = strchr(name, '/');
			*end = '\0';
			assert((pw = getpwnam(name)));
			free(name);
		}

		rest = strchr(tmp, '/') + 1;
		tmp2 = strjoin("/", pw->pw_dir, rest, NULL);
	} else {
		tmp2 = strdup_null(tmp);
	}

	free(tmp);

	return tmp2;
}

/**
 * Parse the configuration-file
 *
 * @param fname The name of the configuration-file to parse
 * @return zero on success, non-zero on failure
 */
static int parse_config_file(const char *fname)
{
	FILE *fp;
	char *str;
	char *mb = NULL;
	int line = 0;
	int tmperrno = 0;
	lsmbox_error_t error;

	if (!(fp = fopen(fname, "r"))) {
		if (errno == ENOENT)
			errno = 0;

		return errno;
	}

	/* Poison */
	buf[BUF_SIZE - 1] = (char)0xff;

	while ((str = fgets(buf, BUF_SIZE, fp))) {
		/* We got a line larger than 64kB */
		if (buf[BUF_SIZE - 1] != (char)0xff) {
			errno = EFBIG;
			goto EXIT;
		}

		error = E_OK;

		line++;

		/* Ignore all comments */
		if (*str == '#')
			continue;

		str = skip_whitespace(str);
		chop_whitespace(str);

		/* Skip empty lines */
		if (!*str)
			continue;

		/* Remove the trailing newline */
		*(str + strlen(str) - 1) = '\0';

		if (!strncmp(str, SET_STR, strlen(SET_STR))) {
			str = skip_config_option(str, 0);

			if (!strncmp(str, SPOOLFILE_STR,
				     strlen(SPOOLFILE_STR))) {
				str = skip_config_option(str, SKIP_EQ);

				if (!str) {
					error = E_SYNTAX;
				} else {
					free(spoolfilepath);
					spoolfilepath = strdup(str);
				}
			} else if (!strncmp(str, MAILBOX_FOLDER_STR,
					    strlen(MAILBOX_FOLDER_STR))) {
				str = skip_config_option(str, SKIP_EQ);

				if (!str) {
					error = E_SYNTAX;
				} else {
					free(mailfolderpath);
					mailfolderpath = strdup(str);
				}
			} else if (!strncmp(str, MARK_OLD_STR,
					    strlen(MARK_OLD_STR))) {
				markold = 1;
			} else if (!strncmp(str, MH_SEQ_UNSEEN_STR,
				strlen(MH_SEQ_UNSEEN_STR))) {
				str = skip_config_option(str,
							 SKIP_EQ | SKIP_QU);

				if (!str) {
					error = E_SYNTAX;
				} else {
					char *tmp;

					if ((tmp = strrchr(str, '"')))
						*tmp = '\0';

					if ((tmp = strrchr(str, '\'')))
						*tmp = '\0';

					free(unseenstr);
					unseenstr = strdup(str);
				}
			} else if (!strncmp(str, MAIL_COMMAND_STR,
				   strlen(MAIL_COMMAND_STR))) {
				str = skip_config_option(str,
							 SKIP_EQ | SKIP_QU);

				if (!str) {
					error = E_SYNTAX;
				} else {
					char *tmp;

					if ((tmp = strrchr(str, '"')))
						*tmp = '\0';

					if ((tmp = strrchr(str, '\'')))
						*tmp = '\0';

					free(mailcommand);
					mailcommand = strdup(str);
				}
			} else if (!strncmp(str, PADDING_LEN_STR,
					    strlen(PADDING_LEN_STR))) {
				str = skip_config_option(str, SKIP_EQ);

				if (!str) {
					error = E_SYNTAX;
				} else {
					unsigned long tmp;

					tmp = strtoul(str, NULL, 10);

					if ((tmp == LONG_MAX &&
					     errno == ERANGE) ||
					    (tmp > PADDING_MAX))
						error = E_SYNTAX;
					else
						padding = (int)tmp;
				}
			} else {
				error = E_UNKNOWN;
			}
		} else if (!strncmp(str, UNSET_STR, strlen(UNSET_STR))) {
			str = skip_config_option(str, 0);

			if (!strncmp(str, SPOOLFILE_STR,
				     strlen(SPOOLFILE_STR))) {
				free(spoolfilepath);
				spoolfilepath = NULL;
			} else if (!strncmp(str, MAILBOX_FOLDER_STR,
					    strlen(MAILBOX_FOLDER_STR))) {
				free(mailfolderpath);
				mailfolderpath = NULL;
			} else if (!strncmp(str, MARK_OLD_STR,
					    strlen(MARK_OLD_STR))) {
				markold = 0;
			} else if (!strncmp(str, MH_SEQ_UNSEEN_STR,
					    strlen(MH_SEQ_UNSEEN_STR))) {
				free(unseenstr);
				unseenstr = strdup(UNSEEN_STR);
			} else if (!strncmp(str, MAIL_COMMAND_STR,
					    strlen(MAIL_COMMAND_STR))) {
				free(mailcommand);
				mailcommand = NULL;
			} else if (!strncmp(str, PADDING_LEN_STR,
					    strlen(PADDING_LEN_STR))) {
				padding = -1;
			} else {
				error = E_UNKNOWN;
			}
		} else if (!strncmp(str, MAILBOX_STR, strlen(MAILBOX_STR))) {
			char *tmp;

			str = skip_config_option(str, 0);

			if (!str) {
				error = E_SYNTAX;
			} else {
				if (mb)
					tmp = strjoin(" ", mb, str, NULL);
				else
					tmp = strdup_null(str);

				free(mb);
				mb = tmp;
			}
		} else {
			error = E_SYNTAX;
		}

		switch (error) {
		case E_UNKNOWN:
			fprintf(stderr,
				_("%s: Unknown option on line: %d inside: %s; "
				  "ignoring.\n"),
				progname, line, fname);
			break;

		case E_SYNTAX:
			fprintf(stderr,
				_("%s: Syntax error on line: %d inside: %s; "
				  "ignoring.\n"),
				progname, line, fname);
			break;

		case E_OK:
		default:
			break;
		}
	}

	if (mb)
		mailboxes = strsplit(mb, " ", 0);

	free(mb);

EXIT:
	tmperrno = errno;

	if (fclose(fp) == EOF || tmperrno)
		errno = tmperrno;

	return errno;
}

/**
 * Parse a mailbox in Maildir format
 *
 * @param mbox A mailstat structure for the mailbox
 * @return zero on success, non-zero on failure
 */
static int parse_maildir(struct mailstat *mbox)
{
	DIR *dp;
	char *tmp;
	struct dirent *de;

	mbox->total = 0;
	mbox->new = 0;
	mbox->old = 0;

	bugfile = mbox->name;

	tmp = strjoin("/", mbox->name, "cur", NULL);

	if (!(dp = opendir(tmp))) {
		if (errno == ENOENT)
			errno = 0;

		free(tmp);
		return errno;
	}

	while ((de = readdir(dp))) {
		struct stat st;
		char *fname;
		char *p;

		if (*de->d_name == '.')
			continue;

		fname = strjoin("/", tmp, de->d_name, NULL);

		if (stat(fname, &st) != -1) {
			free(fname);

			if (!S_ISREG(st.st_mode))
				continue;
		} else {
			free(fname);

			if (errno == ENOENT)
				errno = 0;
			else
				goto EXIT;

			continue;
		}

		mbox->total++;

		if (!(p = strstr(de->d_name, ":2,")) || !strchr(p, 'S'))
			mbox->old++;

		if (eventcheck() && quit)
			goto EXIT;
	}

	if (errno)
		goto EXIT;

	free(tmp);

	tmp = strjoin("/", mbox->name, "new", NULL);

	if (!(dp = opendir(tmp))) {
		free(tmp);

		if (errno == ENOENT)
			errno = 0;

		return errno;
	}

	while ((de = readdir(dp))) {
		struct stat st;
		char *fname;

		if (*de->d_name == '.')
			continue;

		fname = strjoin("/", tmp, de->d_name, NULL);

		if (stat(fname, &st) != -1) {
			free(fname);

			if (!S_ISREG(st.st_mode))
				continue;
		} else {
			free(fname);

			if (errno == ENOENT)
				errno = 0;
			else
				goto EXIT;

			continue;
		}

		mbox->total++;
		mbox->new++;
	}

EXIT:
	free(tmp);

	if (errno)
		return errno;

#ifdef CLOSEDIR_VOID
	closedir(dp);
#else
	if (closedir(dp))
		return errno;
#endif /* CLOSEDIR_VOID */

	return 0;
}

/**
 * Parse a mailbox in MH format
 *
 * @param mbox A mailstat structure for the mailbox
 * @return zero on success, non-zero on failure
 */
static int parse_mhdir(struct mailstat *mbox)
{
	DIR *dp;
	FILE *fp;
	char *fname;
	struct dirent *de;
	unsigned long *newlist = NULL;
	unsigned long nllen = 0;

	mbox->total = 0;
	mbox->new = 0;
	mbox->old = 0;

	bugfile = mbox->name;

	fname = strjoin("/", mbox->name, ".mh_sequences", NULL);

	if (!(fp = fopen(fname, "r"))) {
		free(fname);

		if (errno == ENOENT)
			errno = 0;
		else
			return errno;
	} else {
		char *seq = NULL;

		free(fname);

		/* Poison */
		buf[BUF_SIZE - 1] = (char)0xff;

		while ((seq = fgets(buf, BUF_SIZE, fp))) {
			/* We got a line larger than 64kB */
			if (buf[BUF_SIZE - 1] != (char)0xff) {
				errno = EFBIG;
				return errno;
			}

			if (!strncmp(seq, unseenstr, strlen(unseenstr)))
				break;
			else
				seq = NULL;
		}

		if (errno) {
			if (errno == ENOENT)
				errno = 0;
			else
				return errno;
		}

		if (fclose(fp) == EOF)
			return errno;

		if (seq) {
			seq += strlen(unseenstr);

			if (*seq && *seq == ':')
				seq++;
		}
		while (seq && *seq) {
			unsigned long *p = NULL;
			unsigned long x, y;
			unsigned long len;
			unsigned long i;

			x = strtoul(seq, &seq, 10);
			if (x == LONG_MAX && errno == ERANGE)
				return errno;

			if (!*seq || *seq != '-') {
				y = x;
			} else {
				seq++;

				y = strtoul(seq, &seq, 10);

				if (y == LONG_MAX && errno == ERANGE)
					return errno;
			}

			seq = skip_whitespace(seq);

			len = y - x + 1;

			if ((p = realloc(newlist,
					 (nllen + len) * sizeof x))) {
				newlist = p;
			} else {
				free(newlist);
				return errno;
			}

			for (i = 0; i < nllen; i++) {
				if (newlist[i] == x) {
					break;
				} else if (newlist[i] > x) {
					memmove(&newlist[i] + len,
						&newlist[i],
						(nllen - i) * sizeof x);
					break;
				}
			}

			while (x <= y) {
				newlist[i] = x;
				i++;
				x++;
			}

			nllen += len;
		}
	}

	if (!(dp = opendir(mbox->name))) {
		if (errno == ENOENT)
			errno = 0;

		return errno;
	}

	while ((de = readdir(dp))) {
		unsigned long n;
		struct stat st;

		if (strspn(de->d_name, "0123456789") != strlen(de->d_name))
			continue;

		fname = strjoin("/", mbox->name, de->d_name, NULL);

		if (stat(fname, &st) != -1) {
			free(fname);

			if (!S_ISREG(st.st_mode))
				continue;
		} else {
			free(fname);

			if (errno == ENOENT)
				errno = 0;
			else
				goto EXIT;

			continue;
		}

		mbox->total++;

		n = strtoul(de->d_name, NULL, 10);

		if (n == LONG_MAX && errno == ERANGE)
			goto EXIT;

		if (bsearch(&n, newlist, nllen, sizeof *newlist,
			    ulongcmp)) {
			fname = strjoin("/", mbox->name, de->d_name, NULL);

			if (!(fp = fopen(fname, "r"))) {
				free(fname);

				if (errno == ENOENT)
					errno = 0;
				else
					return errno;
			} else {
				char *str = NULL;
				int old = 0;

				free(fname);

				/* Poison */
				buf[BUF_SIZE - 1] = (char)0xff;

				while ((str = fgets(buf, BUF_SIZE, fp))) {
					/* We got a line larger than 64kB */
					if (buf[BUF_SIZE - 1] != (char)0xff) {
						errno = EFBIG;
						goto EXIT;
					}

					if (*str == '\n')
						break;
					else if (!strncmp(str, STATUS_STR,
						 strlen(STATUS_STR))) {
						char *tmp = str +
							    strlen(STATUS_STR);

						if (strchr(tmp, 'O')) {
							old = 1;
							break;
						}
					}
				}

				if (old)
					mbox->old++;
				else
					mbox->new++;

				if (fclose(fp) == EOF)
					goto EXIT;
			}
		}

		if (eventcheck() && quit)
			goto EXIT;
	}

EXIT:
	if (errno)
		return errno;

#ifdef CLOSEDIR_VOID
	closedir(dp);
#else
	if (closedir(dp))
		return errno;
#endif /* CLOSEDIR_VOID */

	return 0;
}

/**
 * Parse a mailbox in mbox/MMDF format
 *
 * @param mbox A mailstat structure for the mailbox
 * @return zero on success, non-zero on failure
 */
static int parse_mailbox(struct mailstat *mbox)
{
	FILE *fp;
	char *str;
	long clen = 0;
	int old = 0;
	int mread = 0;
	int msgbody = 0;
	int tmperrno = 0;

	mbox->total = 0;
	mbox->new = 0;
	mbox->old = 0;

	bugfile = mbox->name;

	if (!(fp = fopen(mbox->name, "r"))) {
		if (errno == ENOENT)
			errno = 0;

		return errno;
	}

	/* Poison */
	buf[BUF_SIZE - 1] = (char)0xff;

	while ((str = fgets(buf, BUF_SIZE, fp))) {
		/* We got a line larger than 64kB */
		if (buf[BUF_SIZE - 1] != (char)0xff) {
			errno = EFBIG;
			goto EXIT;
		}

		if (!strncmp(str, FROM_STR, strlen(FROM_STR))) {
			msgbody = 0;
			mread = 0;
			old = 0;
		} else if (msgbody) {
			continue;
		} else if (!strncmp(str, STATUS_STR, strlen(STATUS_STR))) {
			char *tmp = str + strlen(STATUS_STR);

			if (strchr(tmp, 'R'))
				mread = 1;
			else if (strchr(tmp, 'O'))
				old = 1;
		} else if (!strncmp(str, CONTENT_LENGTH_STR,
				    strlen(CONTENT_LENGTH_STR))) {
			clen = strtoul(str + strlen(CONTENT_LENGTH_STR),
				       NULL, 10);

			if (clen == LONG_MAX && errno == ERANGE)
				goto EXIT;
		} else if (*str == '\n' && !msgbody) {
			msgbody = 1;

			mbox->total++;

			if (old)
				mbox->old++;
			else if (!mread)
				mbox->new++;

			old = 0;
			mread = 0;

			if (eventcheck() && quit)
				goto EXIT;

			if (clen && fseek(fp, clen, SEEK_CUR))
				goto EXIT;

			clen = 0;
		}
	}

EXIT:
	tmperrno = errno;

	if (fclose(fp) == EOF || tmperrno)
		errno = tmperrno;

	return errno;
}

/**
 * Update statistics for all mailboxes in the mboxes array
 *
 * @return 0 on success, errno on failure
 */
static int update_mailbox_stats(void)
{
	int i;
	time_t modtime = 0;

	for (i = 0; mboxes[i].name; i++) {
		char *name = mboxes[i].name;
		struct stat st;
		int retval;

		if (eventcheck() && quit)
			goto EXIT;

		modtime = mboxes[i].atime > mboxes[i].mtime ? mboxes[i].atime :
							      mboxes[i].mtime;

		bugfile = name;

		if (stat(name, &st) == -1) {
			if (errno == ENOENT) {
				if (mboxes[i].exists && i == selected)
					previous_mbox();

				mboxes[i].exists = 0;
				errno = 0;
				continue;
			} else {
				goto EXIT;
			}
		}

		mboxes[i].exists = 1;

		if (S_ISDIR(st.st_mode)) {
			mboxes[i].type = MAILDIR;
		} else if (S_ISREG(st.st_mode)) {
			mboxes[i].type = MBOX;
		} else {
			errno = ENOENT;
			goto EXIT;
		}

		if (mboxes[i].type == MBOX &&
		    st.st_mtime <= mboxes[i].mtime &&
		    st.st_size == mboxes[i].size) {
			continue;
		} else {
			mboxes[i].mtime = st.st_mtime;
			mboxes[i].atime = st.st_atime;
			mboxes[i].size = st.st_size;
		}

		if (mboxes[i].type == MAILDIR) {
			char *tmp = NULL;

			tmp = strjoin("/", name, ".mh_sequences", NULL);

			if (!stat(tmp, &st)) {
				free(tmp);

				if (S_ISREG(st.st_mode))
					mboxes[i].type = MHDIR;
			} else {
				free(tmp);

				if (errno == ENOENT)
					errno = 0;
				else
					continue;
			}

			tmp = strjoin("/", name, ".xmhcache", NULL);

			if (!stat(tmp, &st)) {
				free(tmp);

				if (S_ISREG(st.st_mode))
					mboxes[i].type = MHDIR;
			} else {
				free(tmp);

				if (errno == ENOENT)
					errno = 0;
				else
					continue;
			}

			if (mboxes[i].type == MAILDIR) {
				int ok = 0;

				tmp = strjoin("/", name, "cur", NULL);

				if (!stat(tmp, &st)) {
					free(tmp);

					if (S_ISDIR(st.st_mode))
						ok = 1;
				} else {
					free(tmp);

					if (errno == ENOENT)
						errno = 0;
					else
						goto EXIT;
				}

				tmp = strjoin("/", name, "new", NULL);

				if (!stat(tmp, &st)) {
					free(tmp);

					if (S_ISDIR(st.st_mode))
						ok = 1;
				} else {
					free(tmp);

					if (errno == ENOENT)
						errno = 0;
					else
						goto EXIT;
				}

				tmp = strjoin("/", name, "tmp", NULL);

				if (!stat(tmp, &st)) {
					free(tmp);

					if (S_ISDIR(st.st_mode))
						ok = 1;
				} else {
					free(tmp);

					if (errno == ENOENT)
						errno = 0;
					else
						goto EXIT;
				}

				if (!ok)
					continue;
			}
		}

		switch (mboxes[i].type) {
		case MHDIR:
			retval = parse_mhdir(&mboxes[i]);
			break;

		case MAILDIR:
			retval = parse_maildir(&mboxes[i]);
			break;

		case MBOX:
		default:
			retval = parse_mailbox(&mboxes[i]);
			break;
		}

		if (retval || quit)
			goto EXIT;

		if (mboxes[i].type == MBOX) {
			static struct utimbuf utbuf;

			utbuf.actime = mboxes[i].atime;
			utbuf.modtime = mboxes[i].mtime;

			if (utime(mboxes[i].name, &utbuf)) {
				if (errno == EACCES || errno == ENOENT)
					errno = 0;
				else
					goto EXIT;
			}
		}
	}

	if (selected >= i)
		selected = i - 1;

EXIT:
	return errno;
}

/**
 * Show statistics for all mailboxes in the mboxes array
 *
 * @param now the time show_mailbox_stats was called
 * @return 0 on success, errno on failure
 */
static int show_mailbox_stats(const time_t now, const int width)
{
	int ypos = 0;
	int i;
	unsigned long new;
	struct mailstat totals;
	time_t modtime = 0;

	if (!selectedtimer)
		selectedtimer = now;
	else if (now - selectedtimer > SELECTED_TIME)
		selected = -1;

	totals.total = 0;
	totals.new = 0;
	totals.old = 0;

	bold_on();
	output_string_padded(_("mailbox:"), width);

	output_string("%s%s:%s\n",
		nototal ? "" : _("total:      "),
		(markold || noold) ? _("new") : _("unread"),
		(markold && !noold) ? _("        old:") : "");
	bold_off();

	for (i = 0, ypos = 1; mboxes[i].name; i++) {
		char *tmp = NULL;

		if (!mboxes[i].exists)
			continue;

#if NCURSES_MOUSE_VERSION == 1
		/* On mouse-click, execute current */
		if (ypos == mouseevent.y &&
		    (mouseevent.bstate & BUTTON1_CLICKED)) {
			execute = i;
			mouseevent.y = 0;
		}
#endif /* NCURSES_MOUSE_VERSION == 1 */

#if HAVE_WORKING_FORK
		if (execute == i && mailcommand) {
			pid_t pid = fork();

			execute = -1;

			if (pid == -1) {
				goto EXIT;
			} else if (pid == 0) {
				tmp = strjoin(" ", mailcommand,
					      mboxes[i].name, NULL);
				execl("/bin/sh", "sh", "-c", tmp, NULL);
				exit(errno);
			} else {
				/* The return-value of the child is of no
				 * importance, but bail out if we called
				 * waitpid with invalid options
				 */
				if (waitpid(pid, NULL,
					    WNOHANG | WUNTRACED) == -1) {
					if (errno == EINVAL)
						goto EXIT;
					else
						errno = 0;
				}
			}
		}
#endif /* HAVE_WORKING_FORK */

		if (markold || noold)
			new = mboxes[i].new;
		else
			new = mboxes[i].new + mboxes[i].old;

		if (newonly && !new)
			continue;

		if (!nosummary) {
			totals.total += mboxes[i].total;
			totals.new += new;
			totals.old += mboxes[i].old;
		}

		tmp = basenamep(mboxes[i].name);

		modtime = mboxes[i].atime > mboxes[i].mtime ? mboxes[i].atime :
							      mboxes[i].mtime;

		hilight_on(now - modtime <= HILIGHT_TIME, (selected == i));

		output_string_padded(shortnames ? tmp : mboxes[i].name, width);

		if (!nototal)
			output_string("%-10lu  ", mboxes[i].total);

		output_string("%-10lu", new);

		if (markold && !noold)
			output_string("  %-10lu", mboxes[i].old);

		output_string("\n");
		hilight_off();

		ypos++;
	}

	if (!nosummary) {
		output_string("\n");
		output_string_padded(_("Summary:"), width);

		if (!nototal)
			output_string("%-10lu  ", totals.total);

		output_string("%-10lu", totals.new);

		if (markold && !noold)
			output_string("  %-10lu\n", totals.old);

		output_string("\n");
	}

	update_screen();

EXIT:
	return errno;
}

/**
 * The program's main-function
 *
 * @param argc The number of arguments
 * @param argv The arguments
 * @return 0 on success, errno on failure
 */
int main(int argc, char *argv[])
{
	int optc;
	int opt_index;
	int tmp_errno;
	size_t i, start, end;
	time_t oldtime, now;
	int maxlen = 0;

	char **list = NULL;
	char *userrcpath = NULL;

	errno = 0;

	/* Initialise support for locales, and set the program-name */
	if (init_locales(PACKAGE_NAME))
		goto EXIT;

	homepath = strdup_null(getenv("HOME"));
	unseenstr = strdup(UNSEEN_STR);

	if (homepath) {
		userrcpath = strjoin(NULL, homepath, "/.lsmboxrc", NULL);

		if (parse_config_file(userrcpath))
			goto EXIT;
	}

	/* Parse the command-line options */
	while ((optc = getopt_long(argc, argv, OPTSTR,
				   options, &opt_index)) != -1) {
		unsigned long tmp = 0;

		switch (optc) {
		case 'c':
			continuous = 1;
			break;

		case 'i':
			tmp = strtoul(optarg, NULL, 10);

			if (tmp == LONG_MAX && errno == ERANGE)
				goto EXIT;

			mailcheck = (time_t)tmp;
			break;

		case 'n':
			newonly = 1;
			break;

		case 'o':
			noold = 1;
			break;

		case 'p':
			if (!optarg || !strlen(optarg)) {
				padding = 0;
				break;
			}

			tmp = strtoul(optarg, NULL, 10);

			if ((tmp == LONG_MAX && errno == ERANGE) ||
			    (tmp > PADDING_MAX))
				goto EXIT;

			padding = (int)tmp;
			break;

		case 's':
			nosummary = 1;
			break;

		case 'S':
			shortnames = 1;
			break;

		case 't':
			nototal = 1;
			break;

		case 'h':
			usage();
			goto EXIT;

		case 'V':
			version();
			goto EXIT;

		default:
			usage();
			errno = EINVAL;
			goto EXIT;
		}
	}

	if (!spoolfilepath) {
		if (!(spoolfilepath = strdup_null(getenv("MAIL")))) {
			spoolfilepath = strdup_null(SPOOLFILE_PATH);
			spoolfilepath = strjoin("/", SPOOLFILE_PATH,
						get_user_name(), NULL);
		}
	}

	if (!mailfolderpath) {
		if (homepath)
			mailfolderpath = strjoin("/", homepath,
						 MAILBOX_FOLDER_PATH, NULL);
		else
			mailfolderpath = strdup(".");
	}

	if (!mailcommand)
		mailcommand = strdup(MAIL_COMMAND);

	if ((argv[optind] == NULL || (argc < 2)) && !mailboxes) {
		usage();
		errno = EINVAL;
		goto EXIT;
	}

	if (argv[optind]) {
		start = optind;
		end = argc;
		list = argv;
	} else {
		start = 0;

		for (end = 0; mailboxes[end]; end++)
			;

		list = mailboxes;
	}

	if (continuous) {
		if (!init_curses())
			goto EXIT;

		if (!mailcheck) {
			unsigned long tmp2 = MAILCHECK_DEFAULT;
			char *tmp = NULL;

			if ((tmp = strdup_null(getenv("MAILCHECK")))) {
				tmp2 = strtoul(tmp, NULL, 10);

				if (tmp2 == LONG_MAX && errno == ERANGE) {
					errno = 0;
					tmp2 = MAILCHECK_DEFAULT;
				}

				free(tmp);
			}

			mailcheck = (time_t)tmp2;
		}

		/* Can't let the intervals be too small */
		if (mailcheck < MAILCHECK_MINIMUM)
			mailcheck = (time_t)MAILCHECK_MINIMUM;

		/* Hey, that's a laaaarge interval, but the user wants
		 * something really large, so we better obey...
		 */
		if (mailcheck > (time_t)INT_MAX)
			mailcheck = (time_t)INT_MAX;
	}

	assert(mboxes = calloc(end - start + 1, sizeof (struct mailstat)));

	for (i = start; i < end; i++) {
		int len;

		mboxes[i - start].name = path_expand(list[i]);

		if (shortnames)
			len = strlen(basenamep(mboxes[i - start].name));
		else
			len = strlen(mboxes[i - start].name);

		if (len > maxlen)
			maxlen = len;
	}

	if (strlen(_("mailbox:")) > maxlen)
		maxlen = strlen(_("mailbox:"));

	if (strlen(_("Summary:")) > maxlen)
		maxlen = strlen(_("Summary:"));

	/* old behaviour */
	if (padding == -1)
		maxlen = 43;
	else if ((padding >= 0) && (padding > maxlen))
		maxlen = padding;

	oldtime = 0;
	now = 0;

	do {
		if (time(&now) == (time_t)-1)
			break;

		/* Update mailstats every mailcheck seconds */
		if ((eventcheck() && !quit) || now - oldtime > mailcheck) {
			if (update_mailbox_stats())
				break;

			if (show_mailbox_stats(now, maxlen))
				break;

			if (time(&oldtime) == (time_t)-1)
				break;
		}

		/* Check for a keypress every second */
		if (!quit && !errno)
			(void)sleep((unsigned int)1);

		if (errno == EINTR || errno == ESRCH)
			errno = 0;
	} while (!errno && continuous && !quit);

	tmp_errno = errno;

	if (continuous)
		reset_curses();

	errno = tmp_errno;

	if (errno == EFBIG)
		fprintf(stderr,
			_("%s: %s contains a line longer than %d characters. "
			  "Aborting.\n"),
			progname, bugfile, BUF_SIZE);
	else if (errno == EFAULT)
		fprintf(stderr,
			_("%s: time() called with invalid pointer. "
			  "Aborting.\n"),
			progname);
	else if (errno && errno != ENOENT)
		fprintf(stderr,
			_("%s: An error occured when processing %s: %s. "
			  "Aborting.\n"),
			progname, bugfile, strerror(errno));

EXIT:
	for (i = 0; mboxes && mboxes[i].name; i++)
		free(mboxes[i].name);

	free(mboxes);
	free(spoolfilepath);
	free(userrcpath);
	free(mailfolderpath);
	free(unseenstr);
	strfreev(mailboxes);

	return errno;
}
