/* $Id$ */
/* Copyright (c) 2011-2018 Pierre Pronchery <khorben@defora.org> */
/* This file is part of DeforaOS Desktop Mailer */
/* 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 COPYRIGHT HOLDERS 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 COPYRIGHT
 * HOLDER 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. */
/* TODO:
 * - import mails automatically from the spool to the inbox */



#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <glib.h>
#include "Mailer/account.h"

#define min(a, b) ((a) < (b) ? (a) : (b))


/* Mbox */
/* private */
#define _FOLDER_CNT 4


/* types */
typedef enum _ParserContext
{
	PC_FROM,	/* while pos < 6 not sure => fallback body or garbage */
	PC_HEADER,	/* inside a header */
	PC_BODY,	/* inside a body */
	PC_GARBAGE	/* inside crap */
} ParserContext;

typedef struct _AccountPlugin Mbox;

struct _AccountMessage
{
	AccountPluginHelper * helper;

	Message * message;

	/* Mbox */
	size_t offset;
	size_t body_offset;
	size_t body_length;
};

struct _AccountFolder
{
	Folder * folder;

	/* Mbox */
	Mbox * mbox;
	AccountConfig * config;
	AccountMessage ** messages;
	size_t messages_cnt;

	/* refresh */
	time_t mtime;
	GIOChannel * channel;
	int source;

	/* parsing */
	size_t offset;
	ParserContext context;
	AccountMessage * message;
	size_t pos; /* context-dependant */
	char * str;

	/* interface */
	char * pixbuf;
};

struct _AccountPlugin
{
	AccountPluginHelper * helper;

	AccountConfig * config;

	AccountFolder folders[_FOLDER_CNT];

	/* refresh */
	unsigned int timeout;
};


/* constants */
#define MBOX_REFRESH_TIMEOUT	5000

static AccountConfig const _mbox_config[] =
{
	{ "mbox",	"Inbox file",		ACT_FILE,	NULL },
	{ "spool",	"Spool file",		ACT_FILE,	NULL },
	{ "draft",	"Draft mails file",	ACT_FILE,	NULL },
	{ "sent",	"Sent mails file",	ACT_FILE,	NULL },
	{ "trash",	"Deleted mails file",	ACT_FILE,	NULL },
	{ NULL,		NULL,			0,		NULL }
};

static const struct
{
	FolderType type;
	char const * name;
	char const * icon;
	unsigned int config;
} _mbox_folder_defaults[_FOLDER_CNT] =
{
	{ FT_INBOX,	"Inbox",	"stock_inbox",	0 },
	{ FT_DRAFTS,	"Drafts",	"stock_drafts",	2 },
	{ FT_SENT,	"Sent",		"stock_sent",	3 },
	{ FT_TRASH,	"Trash",	"stock_trash",	4 }
};


/* plug-in */
static Mbox * _mbox_init(AccountPluginHelper * helper);
static int _mbox_destroy(Mbox * mbox);
static AccountConfig * _mbox_get_config(Mbox * mbox);
static char * _mbox_get_source(Mbox * mbox, AccountFolder * folder,
		AccountMessage * message);
static int _mbox_start(Mbox * mbox);
static void _mbox_stop(Mbox * mbox);
static int _mbox_refresh(Mbox * mbox, AccountFolder * folder,
		AccountMessage * message);

AccountPluginDefinition account_plugin =
{
	"MBOX",
	"Local folders",
	NULL,
	NULL,
	_mbox_config,
	_mbox_init,
	_mbox_destroy,
	_mbox_get_config,
	_mbox_get_source,
	_mbox_start,
	_mbox_stop,
	_mbox_refresh
};


/* prototypes */
/* callbacks */
static gboolean _folder_idle(gpointer data);
static gboolean _folder_watch(GIOChannel * source, GIOCondition condition,
		gpointer data);


/* AccountMessage */
/* private */
/* prototypes */
static AccountMessage * _message_new(AccountPluginHelper * helper,
		Folder * folder, off_t offset);
static void _message_delete(AccountMessage * message);

static int _message_set_body(AccountMessage * message, off_t offset,
		size_t length);
static int _message_set_header(AccountMessage * message, char const * header);


/* Mbox */
/* functions */
/* mbox_init */
static Mbox * _mbox_init(AccountPluginHelper * helper)
{
	Mbox * mbox;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s()\n", __func__);
#endif
	if((mbox = calloc(1, sizeof(*mbox))) == NULL)
		return NULL;
	mbox->helper = helper;
	mbox->timeout = MBOX_REFRESH_TIMEOUT;
	if((mbox->config = malloc(sizeof(_mbox_config))) == NULL)
	{
		free(mbox);
		return NULL;
	}
	memcpy(mbox->config, &_mbox_config, sizeof(_mbox_config));
	return mbox;
}


/* mbox_destroy */
static int _mbox_destroy(Mbox * mbox)
{
	size_t i;
	AccountFolder * mf;
	size_t j;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s()\n", __func__);
#endif
	if(mbox == NULL) /* XXX _mbox_destroy() may be called uninitialized */
		return 0;
	_mbox_stop(mbox);
	for(i = 0; i < _FOLDER_CNT; i++)
	{
		mf = &mbox->folders[i];
		for(j = 0; j < mf->messages_cnt; j++)
			_message_delete(mf->messages[j]);
		free(mf->messages);
		mf->messages = NULL;
		mf->messages_cnt = 0;
	}
	free(mbox);
	return 0;
}


/* mbox_get_config */
static AccountConfig * _mbox_get_config(Mbox * mbox)
{
	return mbox->config;
}


/* mbox_get_source */
static char * _mbox_get_source(Mbox * mbox, AccountFolder * folder,
		AccountMessage * message)
{
	char * ret = NULL;
	char const * filename = folder->config->value;
	FILE * fp;
	size_t len;

	if(message->body_offset < message->offset)
		return NULL;
	if((fp = fopen(filename, "r")) == NULL)
	{
		mbox->helper->error(mbox->helper->account, filename, 1);
		return NULL;
	}
	len = message->body_offset - message->offset + message->body_length;
	if(fseek(fp, message->offset, SEEK_SET) == 0
			&& (ret = malloc(len + 1)) != NULL
			&& fread(ret, sizeof(*ret), len, fp) == len)
		ret[len] = '\0';
	else
		free(ret);
	if(fclose(fp) != 0)
	{
		mbox->helper->error(mbox->helper->account, filename, 1);
		free(ret);
		ret = NULL;
	}
	return ret;
}


/* mbox_start */
static int _mbox_start(Mbox * mbox)
{
	AccountPluginHelper * helper = mbox->helper;
	size_t i;
	AccountFolder * af;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s()\n", __func__);
#endif
	_mbox_stop(mbox);
	for(i = 0; i < _FOLDER_CNT; i++)
	{
		af = &mbox->folders[i];
		af->config = &mbox->config[_mbox_folder_defaults[i].config];
#ifdef DEBUG
		fprintf(stderr, "DEBUG: %s() \"%s\"\n", __func__,
				(char *)af->config->value);
#endif
		if(af->config->value == NULL)
			continue;
		af->folder = helper->folder_new(helper->account, af, NULL,
				_mbox_folder_defaults[i].type,
				_mbox_folder_defaults[i].name);
		af->mbox = mbox;
		af->source = g_idle_add(_folder_idle, af);
	}
	return 0;
}


/* mbox_stop */
static void _mbox_stop(Mbox * mbox)
{
	size_t i;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s()\n", __func__);
#endif
	/* FIXME really implement */
	for(i = 0; i < _FOLDER_CNT; i++)
	{
		if(mbox->folders[i].source != 0)
			g_source_remove(mbox->folders[i].source);
		mbox->folders[i].source = 0;
	}
}


/* mbox_refresh */
static int _mbox_refresh(Mbox * mbox, AccountFolder * folder,
		AccountMessage * message)
{
	char const * filename = folder->config->value;
	FILE * fp;
	char * buf;
	size_t size;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s(%p, %p)\n", __func__, (void *)folder,
			(void *)message);
#endif
	if(message == NULL)
		return 0;
	mbox->helper->message_set_body(message->message, NULL, 0, 0);
	/* XXX we may still be reading the file... */
	if((fp = fopen(filename, "r")) == NULL)
		return -mbox->helper->error(NULL, strerror(errno), 1);
	if(message->body_offset != 0 && message->body_length > 0
			&& fseek(fp, message->body_offset, SEEK_SET) == 0
			&& (buf = malloc(message->body_length)) != NULL)
	{
		if((size = fread(buf, 1, message->body_length, fp)) > 0)
			mbox->helper->message_set_body(message->message, buf,
					size, 1);
		free(buf);
	}
	fclose(fp);
	return 0;
}


/* AccountMessage */
/* functions */
/* message_new */
static AccountMessage * _message_new(AccountPluginHelper * helper,
		Folder * folder, off_t offset)
{
	AccountMessage * message;

	if((message = malloc(sizeof(*message))) == NULL)
	{
		/* FIXME catch error */
		return NULL;
	}
	message->helper = helper;
	message->message = helper->message_new(helper->account, folder,
			message);
	message->offset = offset;
	message->body_offset = 0;
	message->body_length = 0;
	if(message->message == NULL)
	{
		_message_delete(message);
		return NULL;
	}
	return message;
}


/* message_delete */
static void _message_delete(AccountMessage * message)
{
	message->helper->message_delete(message->message);
	free(message);
}


/* message_set_body */
static int _message_set_body(AccountMessage * message, off_t offset,
		size_t length)
{
#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s(%p, %lu, %lu)\n", __func__, (void*)message,
			offset, length);
#endif
	message->body_offset = offset;
	message->body_length = length;
	return 0;
}


/* message_set_header */
static int _message_set_header(AccountMessage * message, char const * header)
{
	return message->helper->message_set_header(message->message, header);
}


/* functions */
/* callbacks */
/* folder_idle */
static gboolean _folder_idle(gpointer data)
{
	AccountFolder * folder = data;
	Mbox * mbox = folder->mbox;
	struct stat st;
	char const * filename = folder->config->value;
	GError * error = NULL;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s() stat(\"%s\")\n", __func__, filename);
#endif
	if(filename == NULL || filename[0] == '\0')
		return FALSE;
	if(stat(filename, &st) != 0)
	{
		mbox->helper->error(NULL, strerror(errno), 1);
		folder->source = g_timeout_add(mbox->timeout, _folder_idle,
				folder);
		return FALSE;
	}
	if(st.st_mtime == folder->mtime)
	{
		folder->source = g_timeout_add(mbox->timeout, _folder_idle,
				folder);
		return FALSE;
	}
	folder->mtime = st.st_mtime; /* FIXME only when done */
	if(folder->channel == NULL)
		if((folder->channel = g_io_channel_new_file(filename, "r",
						&error)) == NULL)
	{
		mbox->helper->error(NULL, error->message, 1);
		g_error_free(error);
		folder->source = g_timeout_add(mbox->timeout, _folder_idle,
				folder);
		return FALSE;
	}
	g_io_channel_set_encoding(folder->channel, NULL, NULL);
	folder->source = g_io_add_watch(folder->channel, G_IO_IN, _folder_watch,
			folder);
	return FALSE;
}


/* folder_watch */
static void _watch_parse(AccountFolder * folder, char const buf[], size_t read);
static int _parse_append(AccountFolder * folder, char const buf[], size_t len);
static void _parse_from(AccountFolder * folder, char const buf[], size_t read,
		size_t * i);
static void _parse_garbage(AccountFolder * folder, char const buf[],
		size_t read, size_t * i);
static void _parse_header(AccountFolder * folder, char const buf[], size_t read,
		size_t * i);
static void _parse_body(AccountFolder * folder, char const buf[], size_t read,
		size_t * i);
static AccountMessage * _folder_message_add(AccountFolder * folder,
		off_t offset);

static gboolean _folder_watch(GIOChannel * source, GIOCondition condition,
		gpointer data)
{
	AccountFolder * folder = data;
	Mbox * mbox = folder->mbox;
	char buf[BUFSIZ];
	size_t read;
	GError * error = NULL;
	GIOStatus status;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s() \"%s\"\n", __func__,
			(char const *)folder->config->value);
#endif
	if(condition != G_IO_IN)
		return FALSE; /* FIXME implement message deletion */
	status = g_io_channel_read_chars(source, buf, sizeof(buf), &read,
			&error);
	switch(status)
	{
		case G_IO_STATUS_ERROR:
			mbox->helper->error(NULL, error->message, 1);
			g_error_free(error);
			/* FIXME new timeout 1000 function after invalidating
			 * mtime */
			return FALSE;
		case G_IO_STATUS_AGAIN:
			return TRUE; /* should not happen */
		case G_IO_STATUS_EOF:
		case G_IO_STATUS_NORMAL:
			break;
	}
	_watch_parse(folder, buf, read);
	if(status == G_IO_STATUS_EOF)
	{
		/* XXX should not be necessary here */
		if(folder->message != NULL)
			_message_set_body(folder->message,
					folder->message->body_offset,
					folder->offset
					- folder->message->body_offset);
		if(g_io_channel_shutdown(source, TRUE, &error)
				!= G_IO_STATUS_NORMAL && error != NULL)
		{
			mbox->helper->error(NULL, error->message, 1);
			g_error_free(error);
		}
		g_io_channel_unref(source);
		folder->channel = NULL;
		folder->source = g_timeout_add(mbox->timeout, _folder_idle,
				folder);
		return FALSE;
	}
	return TRUE;
}

static void _watch_parse(AccountFolder * folder, char const buf[], size_t read)
{
	size_t i = 0;

	while(i < read)
		switch(folder->context)
		{
			case PC_FROM:
				_parse_from(folder, buf, read, &i);
				break;
			case PC_GARBAGE:
				_parse_garbage(folder, buf, read, &i);
				break;
			case PC_HEADER:
				_parse_header(folder, buf, read, &i);
				break;
			case PC_BODY:
				_parse_body(folder, buf, read, &i);
				break;
		}
	folder->offset += read;
}

static int _parse_append(AccountFolder * folder, char const buf[], size_t len)
{
	char * p;

	if((p = realloc(folder->str, folder->pos + len + 1)) == NULL)
		return -1; /* FIXME track error */
	folder->str = p;
	memcpy(&folder->str[folder->pos], buf, len);
	folder->pos += len;
	folder->str[folder->pos] = '\0';
	return 0;
}

static void _parse_context(AccountFolder * folder, ParserContext context)
{
	folder->context = context;
	free(folder->str);
	folder->str = NULL;
	folder->pos = 0;
}

static void _parse_from(AccountFolder * folder, char const buf[], size_t read,
		size_t * i)
{
	static char const from[] = "From ";
	size_t m;

	for(m = 0; *i + m < read && m < sizeof(from) - 1 && buf[*i + m] != '\n';
			m++);
	_parse_append(folder, &buf[*i], m);
	*i += m;
	if(*i == read) /* not enough data read */
		return;
	if(folder->pos < sizeof(from) - 1 /* early newline */
			|| strncmp(folder->str, from, sizeof(from) - 1) != 0)
	{
		if(folder->message != NULL)
			folder->context = PC_BODY;
		else
			folder->context = PC_GARBAGE;
		return; /* switch context immediately */
	}
	for(m = 0; *i + m < read && buf[*i + m] != '\n'; m++);
	_parse_append(folder, &buf[*i], m);
	*i += m;
	if(*i == read)
		return; /* grab more data XXX is gonna force a check again */
	if(folder->message != NULL)
		_message_set_body(folder->message,
				folder->message->body_offset,
				folder->offset + *i - folder->pos
				- folder->message->body_offset);
	folder->message = _folder_message_add(folder, folder->offset + *i
			- folder->pos);
	_message_set_header(folder->message, folder->str);
	_parse_context(folder, PC_HEADER); /* read headers */
	(*i)++;
}

static void _parse_garbage(AccountFolder * folder, char const buf[],
		size_t read, size_t * i)
{
	for(; *i < read && buf[*i] != '\n'; (*i)++);
	if(*i == read)
		return;
	(*i)++;
	_parse_context(folder, PC_FROM);
}

static void _parse_header(AccountFolder * folder, char const buf[], size_t read,
		size_t * i)
{
	size_t j;

	for(j = *i; j < read && buf[j] != '\n'; j++);
	_parse_append(folder, &buf[*i], j - *i);
	*i = j;
	if(j == read)
		return;
	_message_set_header(folder->message, folder->str);
	_parse_context(folder, (folder->pos == 0) ? PC_FROM : PC_HEADER);
	*i = ++j;
}

static void _parse_body(AccountFolder * folder, char const buf[], size_t read,
		size_t * i)
{
	size_t j;

	for(j = *i; j < read && buf[j] != '\n'; j++);
	if(folder->message->body_offset == 0)
		_message_set_body(folder->message, folder->offset + *i
				- folder->pos, 0);
	_parse_append(folder, &buf[*i], j - *i);
	if(j == read)
	{
		/* TODO skip data instead of storing it */
		*i = j;
		return;
	}
	_parse_context(folder, PC_FROM);
	*i = (j + 1);
}

static AccountMessage * _folder_message_add(AccountFolder * folder,
		off_t offset)
{
	AccountMessage ** p;
	AccountMessage * message;

#ifdef DEBUG
	fprintf(stderr, "DEBUG: %s(\"%s\", %ld)\n", __func__,
			(char const *)folder->config->value, offset);
#endif
	if((p = realloc(folder->messages, sizeof(*p)
					* (folder->messages_cnt + 1))) == NULL)
	{
		/* FIXME track error */
		return NULL;
	}
	folder->messages = p;
	if((message = _message_new(folder->mbox->helper, folder->folder,
					offset)) == NULL)
	{
		/* FIXME track error */
		return NULL;
	}
	folder->messages[folder->messages_cnt++] = message;
	return message;
}
