/* GKrellM
|  Copyright (C) 1999-2021 Bill Wilson
|
|  Author:  Bill Wilson
|  Latest versions might be found at:  https://gkrellm.srcbox.net
|
|
|  GKrellM is free software: you can redistribute it and/or modify it
|  under the terms of the GNU General Public License as published by
|  the Free Software Foundation, either version 3 of the License, or
|  (at your option) any later version.
|
|  GKrellM 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, see http://www.gnu.org/licenses/
|
|
|  Additional permission under GNU GPL version 3 section 7
|
|  If you modify this program, or any covered work, by linking or
|  combining it with the OpenSSL project's OpenSSL library (or a
|  modified version of that library), containing parts covered by
|  the terms of the OpenSSL or SSLeay licenses, you are granted
|  additional permission to convey the resulting work.
|  Corresponding Source for a non-source form of such a combination
|  shall include the source code for the parts of OpenSSL used as well
|  as that of the covered work.
*/

#include "gkrellmd.h"
#include "gkrellmd-private.h"


#if !defined(WIN32)

#define MBOX_MBOX		0
#define MBOX_MAILDIR	1
#define MBOX_MH_DIR		2


typedef struct Mailbox Mailbox;
struct Mailbox
	{
	gchar		*path;
	gchar		*homedir_path;
	gint		mboxtype;
	gboolean	(*check_func)(Mailbox *mbox);
	gint		mail_count;
	gint		new_mail_count;
	gint		old_mail_count;
	gint		prev_mail_count,
				prev_new_mail_count;
	time_t		last_mtime;
	off_t		last_size;
	gboolean	is_internal;	/* Internal mail message (ie: localmachine) */
	gboolean	changed;
	};

static GList	*mailbox_list;

static gint		mail_check_timeout = 5;			/* Seconds */

static gboolean	unseen_is_new = TRUE;			/* Accessed but unread */

static gboolean	mail_need_serve;


  /* Look at a From line to see if it is valid, lines look like:
  |  From sending_address dayofweek month dayofmonth timeofday year
  |  eg: From billw@gkrellm.net Fri Oct 22 13:52:49 2010
  */
static gint
is_From_line(Mailbox *mbox, gchar *buf)
	{
	gchar	sender[512];
	gint	dayofmonth = 0;

	if (strncmp(buf, "From ", 5))
		return FALSE;

	/* In case sending address missing, look for a day of month
	|  number in field 3 or 4 (0 based).
	*/
	sender[0] = '\0';
	if (sscanf(buf, "%*s %*s %*s %d", &dayofmonth) != 1)
		{
		if (sscanf(buf, "%*s %511s %*s %*s %d", sender, &dayofmonth) != 2)
			return FALSE;
		}
	if (dayofmonth < 1 || dayofmonth > 31)
		return FALSE;
	if (strcmp(sender, "MAILER-DAEMON") == 0)
		mbox->is_internal = TRUE;
	return TRUE;
	}


  /* Check if this is a Content-Type-line. If it contains a boundary
  |  field, copy boundary string to buffer (including two leading and
  |  trailing dashes marking the end of a multipart mail) and return
  |  true. Otherwise, return false.
  */
static gint
is_multipart_mail(gchar *buf, gchar *separator)
	{
	gchar *fieldstart;
	gchar *sepstart;
	gint  seplen;

	if (strncmp(buf, "Content-Type: ", 14) != 0)
		return FALSE;
	if (strncmp(&buf[14], "multipart/", 10) != 0)
		return FALSE;
	fieldstart = &buf[14];
	while (*fieldstart!=0)
		{
		while (*fieldstart!=0 && *fieldstart!=';')
			fieldstart++;
		if (*fieldstart==';') fieldstart++;
		while (*fieldstart!=0 && *fieldstart==' ')
			fieldstart++;
		if (strncmp(fieldstart, "boundary=", 9) == 0)
			{
			sepstart = fieldstart + 9;
			if (sepstart[0]=='"')
				{
				sepstart++;
				seplen = 0;
				while (sepstart[seplen]!='"' && sepstart[seplen]>=32)
					seplen++;
				}
			else
				{
				seplen = 0;
				while (sepstart[seplen]!=';' && sepstart[seplen]>32)
					seplen++;
				}
			strcpy(separator,"--");
			strncpy(&separator[2],sepstart,seplen);
			strcpy(&separator[seplen+2],"--");
			return TRUE;
			}
		}
	return FALSE;
	}

static gboolean
mh_sequences_new_count(Mailbox *mbox)
	{
	FILE	*f;
	gchar	buf[1024];
	gchar	*path, *tok;
	gint	n0, n1;

	path = g_strconcat(mbox->path, G_DIR_SEPARATOR_S,
				".mh_sequences", NULL);
	f = fopen(path, "r");
	g_free(path);
	if (!f)
		return FALSE;
	while (fgets(buf, sizeof(buf), f))
		{
		/* Look for unseen sequence like "unseen: 4 7-9 23"
		*/
		if (strncmp(buf, "unseen:", 7))
			continue;
		tok = strtok(buf, " \t\n");
		while ((tok = strtok(NULL, " \t\n")) != NULL)
			{
			if (sscanf(tok, "%d-%d", &n0, &n1) == 2)
				mbox->new_mail_count += n1 - n0 + 1;
			else
				mbox->new_mail_count++;
			}
		break;
		}
	fclose(f);
	return TRUE;
	}

  /* Sylpheed procmsg.h enums MSG_NEW as (1 << 0) and MSG_UNREAD as (1 << 1)
  |  And procmsg_write_flags() in Sylpheeds procmsg.c writes a mail record as
  |  a pair of ints with msgnum first followed by flags.
  */
#define SYLPHEED_MSG_NEW		1
#define SYLPHEED_MSG_UNREAD		2
#define SYLPHEED_MARK_VERSION	2

static gboolean
sylpheed_mark_new_count(Mailbox *mbox)
	{
	FILE	*f;
	gchar	*path;
	gint	msgnum, flags, ver, mark_files = 0;

	path = g_strconcat(mbox->path, G_DIR_SEPARATOR_S,
				".sylpheed_mark", NULL);
	f = fopen(path, "rb");
	g_free(path);
	if (!f)
		return FALSE;

	if (   fread(&ver, sizeof(ver), 1, f) == 1
		&& SYLPHEED_MARK_VERSION == ver
	   )
		{
		while (   fread(&msgnum, sizeof(msgnum), 1, f) == 1
			   && fread(&flags, sizeof(flags), 1, f) == 1
			  )
			{
			if (   (flags & SYLPHEED_MSG_NEW)
				|| ((flags & SYLPHEED_MSG_UNREAD) && unseen_is_new)
			   )
				mbox->new_mail_count += 1;
			++mark_files;
			}
		if (mark_files < mbox->mail_count)
			mbox->new_mail_count += mbox->mail_count - mark_files;
		}
	fclose(f);
	return TRUE;
	}


  /* Check a mh directory for mail. The way that messages are marked as new
  |  depends on the MUA being using.  Only .mh_sequences and .sylpheed_mark
  |  are currently checked, otherwise all mail found is considered new mail.
  */
static gboolean
check_mh_dir(Mailbox *mbox)
	{
	GDir	*dir;
	gchar	*name;

	mbox->mail_count = mbox->new_mail_count = 0;

	if ((dir = g_dir_open(mbox->path, 0, NULL)) == NULL)
		return FALSE;
	while ((name = (gchar *) g_dir_read_name(dir)) != NULL)
		{
		/* Files starting with a digit are messages. */
		if (isdigit((unsigned char)name[0]))
			mbox->mail_count++;
		}
	g_dir_close(dir);

	/* Some MH dir clients use .mh_sequences, others such as mutt or gnus
	|  do not.  For mixed cases, it's a user option to ignore .mh_sequences.
	|  Sylpheed uses .sylpheed_mark.
	*/
	if (   !mh_sequences_new_count(mbox)
		&& !sylpheed_mark_new_count(mbox)
	   )
		mbox->new_mail_count = mbox->mail_count;

	return TRUE;
	}


  /* A maildir has new, cur, and tmp subdirectories.  Any file in new
  |  or cur that does not begin with a '.' is a mail message.  It is
  |  suggested that messages begin with the output of time() (9 digits)
  |  but while mutt and qmail use this standard, procmail does not.
  |  maildir(5) says:
  |      It is a good idea for readers to skip all filenames in
  |      new and cur starting with a dot.  Other than this,
  |      readers should not attempt to parse filenames.
  |  So check_maildir() simply looks for files in new and cur.
  |  But if unseen_is_new flag is set, look for ":2,*S" file suffix where
  |  the 'S' indicates the mail is seen.
  |  See http://cr.yp.to/proto/maildir.html
  */
static gboolean
check_maildir(Mailbox *mbox)
	{
	gchar	path[256], *s;
	gchar	*name;
	GDir	*dir;

	mbox->new_mail_count = 0;
	snprintf(path, sizeof(path), "%s%cnew", mbox->path,
				G_DIR_SEPARATOR);
	if ((dir = g_dir_open(path, 0, NULL)) != NULL)
		{
		while ((name = (gchar *) g_dir_read_name(dir)) != NULL)
			mbox->new_mail_count++;
		g_dir_close(dir);
		}
	mbox->mail_count = mbox->new_mail_count;
	snprintf(path, sizeof(path), "%s%ccur", mbox->path,
				G_DIR_SEPARATOR);
	if ((dir = g_dir_open(path, 0, NULL)) != NULL)
		{
		while ((name = (gchar *) g_dir_read_name(dir)) != NULL)
			{
			mbox->mail_count++;
			if (   unseen_is_new
				&& (   (s = strchr(name, ':')) == NULL
					|| !strchr(s, 'S')
				   )
			   )
				mbox->new_mail_count++;
			}
		g_dir_close(dir);
		}

	if (_GK.debug_level & DEBUG_MAIL)
		g_print(_("mdir %s total=%d old=%d new=%d\n"), mbox->path,
			mbox->mail_count, mbox->old_mail_count, mbox->new_mail_count);
    return TRUE;
	}


  /* Count total mail and old mail in a mailbox.  Old mail can be read
  |  with a Status: R0, or can be accessed and not read with Status: O
  |  So, new mail will be the diff - note that unread mail is not
  |  necessarily new mail.  According to stat() man page:
  |  st_atime is changed by mknod(), utime(), read(), write(), truncate()
  |  st_mtime is changed by mknod(), utime(), write()
  |  But, new mail arriving (writing mailbox) sets st_mtime while reading
  |  the mailbox (mail program reading) sets st_atime.  So the test
  |  st_atime > st_mtime is testing if mbox has been read since last new mail.
  |  Mail readers may restore st_mtime after writing status.
  |  And Netscape mail does status with X-Mozilla-Status: xxxS
  |    where S is bitwise or of status flags:
  |    1: read  2: replied  4: marked  8: deleted
  |
  |  Evolution uses status with X-Evolution: 00000000-xxxx where xxxx status is
  |  a bitfield in hexadecimal (see enum _CamelMessageFlags in evolution/camel
  |  source) and most importantly CAMEL_MESSAGE_SEEN = 1<<4.
  */
  /* test if buf is a status for standard mail, mozilla or evolution
  */
static gboolean
is_status(gchar *buf)
	{
	if (buf[0] != 'S' && buf[0] != 'X')
		return FALSE;

	if (   !strncmp(buf, "Status:", 7)  /* Standard mail clients */
	    || !strncmp(buf, "X-Mozilla-Status:", 17) /* Netscape */
	    || !strncmp(buf, "X-Evolution:", 12)      /* Mozilla */
	   )
	    return TRUE;
	else
	    return FALSE;
	}

static gboolean
status_is_old(gchar *buf)
	{
	gchar	c;
	int tmp;

	/* Standard mail clients
	*/
	if (   !strncmp(buf, "Status:", 7)
		&& (strchr(buf, 'R') || (!unseen_is_new && strchr(buf, 'O')))
	   )
		return TRUE;

	/* Netscape
	*/
	if (!strncmp(buf, "X-Mozilla-Status:", 17))
		{
		c = buf[21];
		if (c < '8')	/* Not deleted */
			c -= '0';
		if (c >= '8' || (c & 0x1))
			return TRUE;
		}

	/* Evolution
	*/
	if (!strncmp(buf, "X-Evolution:", 12))
		{
		sscanf(buf+22, "%04x", &tmp);
		if (tmp & (1<<4))
			return TRUE;
		}

	return FALSE;
	}

  /* test if a mail is marked as deleted
  |  Evolution uses status with X-Evolution: 00000000-xxxx where xxxx status is
  |  a bitfield in hexadecimal (see enum _CamelMessageFlags in evolution/camel source)
  |  and most importantly CAMEL_MESSAGE_DELETED = 1<<1.
  */
static gboolean
status_is_deleted(gchar *buf)
	{
	gint	tmp;

	/* Standard mail clients
	if (   !strncmp(buf, "Status:", 7) )
	*/
	/* Netscape
	if (!strncmp(buf, "X-Mozilla-Status:", 17))
	*/
	/* Evolution
	*/
	if (!strncmp(buf, "X-Evolution:", 12))
		{
		sscanf(buf+22, "%04x", &tmp);
		if (tmp & (1<<1))
			return TRUE;
		/* Junk is not explicitly marked as deleted but is shown as if
		|  where in evolution
		*/
		if (tmp & (1<<7))
			return TRUE;
		}

	return FALSE;
	}

static gboolean
check_mbox(Mailbox *mbox)
	{
	FILE			*f;
	struct utimbuf	ut;
	struct stat		s;
	gchar			buf[1024];
	gchar			mpart_sep[1024];
	gint			in_header	= FALSE;
	gint			marked_read = FALSE;
	gint			is_multipart = FALSE;

	if (stat(mbox->path, &s) != 0)
		{
		mbox->mail_count = mbox->old_mail_count = mbox->new_mail_count = 0;
		mbox->last_mtime = 0;
		mbox->last_size = 0;
		gkrellm_debug(DEBUG_MAIL, "check_mbox can't stat(%s): %s\n",
			mbox->path, g_strerror(errno));
		return FALSE;
		}

	/* If the mailboxes have been modified since last check, count
	|  the new/total messages.
	*/
	if (   s.st_mtime != mbox->last_mtime
		|| s.st_size  != mbox->last_size
	   )
		{
		if ((f = fopen(mbox->path, "r")) == NULL)
			{
			gkrellm_debug(DEBUG_MAIL, "check_mbox can't fopen(%s): %s\n",
				mbox->path, g_strerror(errno));
			return FALSE;
			}
		mbox->mail_count = 0;
		mbox->old_mail_count = 0;
		while(fgets(buf, sizeof(buf), f))
			{
			if (is_multipart && !in_header)
				{
				/* Skip to last line of multipart mail */
				if (strncmp(buf,mpart_sep,strlen(mpart_sep))==0)
					is_multipart = FALSE;
				}
			else if (buf[0] == '\n')
				{
				in_header = FALSE;
				mbox->is_internal = FALSE;
				}
			else if (is_From_line(mbox, buf))
				{
				mbox->mail_count += 1;
				in_header = TRUE;
				marked_read = FALSE;
				}
			else if (in_header && is_status(buf))
				{
				if (status_is_old(buf) && !marked_read)
					{
					mbox->old_mail_count += 1;
					marked_read = TRUE;
				    }
				if (status_is_deleted(buf))
					{
					if (marked_read)
						mbox->old_mail_count -= 1;
					mbox->mail_count -= 1;
				    }
				}
			else if (in_header && mbox->is_internal)
				{
				if (strncmp(buf, "From: Mail System Internal Data", 31) == 0)
					{
					in_header = FALSE;
					mbox->mail_count -= 1;
					mbox->is_internal = FALSE;
					}
				}
			else if (in_header && is_multipart_mail(buf,mpart_sep))
				{
				is_multipart = TRUE;
				}
			}
		fclose(f);

		/* Restore the mbox stat times for other mail checking programs and
		|  so the (st_atime > st_mtime) animation override below will work.
		*/
		ut.actime = s.st_atime;
		ut.modtime = s.st_mtime;
		utime(mbox->path, &ut);

		mbox->last_mtime = s.st_mtime;
		mbox->last_size = s.st_size;
		if (_GK.debug_level & DEBUG_MAIL)
			g_print("mbox read <%s> total=%d old=%d\n",
					mbox->path,
					mbox->mail_count, mbox->old_mail_count);
		}

	/* If mbox has been accessed since last modify a MUA has probably read
	|  the mbox.
	*/
	mbox->new_mail_count = mbox->mail_count - mbox->old_mail_count;
	if (s.st_atime > s.st_mtime)
		{
		mbox->prev_new_mail_count = mbox->new_mail_count;
		}
    return TRUE;
	}


static void
update_mail(GkrellmdMonitor *mon, gboolean force)
	{
	Mailbox		*mbox;
	GList		*list;
	static gint	second_count;

	if (   (!GK.second_tick || (++second_count % mail_check_timeout) != 0)
		&& !force
	   )
		return;

	for (list = mailbox_list; list; list = list->next)
		{
		mbox = (Mailbox *) list->data;
		if (mbox->check_func)
			(*mbox->check_func)(mbox);

		if (   mbox->prev_mail_count != mbox->mail_count
			|| mbox->prev_new_mail_count != mbox->new_mail_count
		   )
			{
			mbox->changed = TRUE;
			mail_need_serve = TRUE;
			gkrellmd_need_serve(mon);
			}
		mbox->prev_mail_count = mbox->mail_count;
		mbox->prev_new_mail_count = mbox->new_mail_count;
		}
	}


static void
get_local_mboxtype(Mailbox *mbox)
	{
	gchar	*path;

	if (*(mbox->path) == '~')
		{
		mbox->homedir_path = mbox->path;
		mbox->path = g_strdup_printf("%s%s", g_get_home_dir(),
					mbox->homedir_path + 1);
		}
	if (g_file_test(mbox->path, G_FILE_TEST_IS_DIR))
		{
		path = g_build_path(G_DIR_SEPARATOR_S, mbox->path, "new", NULL);
		if (g_file_test(path, G_FILE_TEST_IS_DIR))
			mbox->mboxtype = MBOX_MAILDIR;
		else
			mbox->mboxtype = MBOX_MH_DIR;
		g_free(path);
		}
	else
		mbox->mboxtype = MBOX_MBOX;
	}

void
gkrellmd_add_mailbox(gchar *path)
	{
	Mailbox	*mbox;

	if (!path || !*path)
		return;
	mbox = g_new0(Mailbox, 1);
	mbox->path = g_strdup(path);
	get_local_mboxtype(mbox);

	if (mbox->mboxtype == MBOX_MAILDIR)
		mbox->check_func = check_maildir;
	else if (mbox->mboxtype == MBOX_MH_DIR)
		mbox->check_func = check_mh_dir;
	else
		mbox->check_func = check_mbox;

	mailbox_list = g_list_append(mailbox_list, mbox);
	gkrellmd_add_serveflag_done(&mbox->changed);
	}

/* ============================================================= */

static void
serve_mail_data(GkrellmdMonitor *mon, gboolean first_serve)
	{
	Mailbox		*mbox;
	GList		*list;
	gchar		*line;

	if ((!mail_need_serve && !first_serve) || !mailbox_list)
		return;
	gkrellmd_set_serve_name(mon, "mail");
	for (list = mailbox_list; list; list = list->next)
		{
		mbox = (Mailbox *) list->data;
		if (mbox->changed || first_serve)
			{
			line = g_strdup_printf("%s %d %d\n", mbox->homedir_path ?
						mbox->homedir_path : mbox->path,
						mbox->mail_count, mbox->new_mail_count);
			gkrellmd_serve_data(mon, line);
			g_free(line);
			}
		}
	}

static void
serve_mail_setup(GkrellmdMonitor *mon)
	{
	GkrellmdClient	*client = mon->privat->client;
	GList			*list;
	Mailbox			*mbox;
	gchar			*line;

	gkrellmd_send_to_client(client, "<mail_setup>\n");
	for (list = mailbox_list; list; list = list->next)
		{
		mbox = (Mailbox *) list->data;
		line = g_strdup_printf("%s\n", mbox->homedir_path ?
					mbox->homedir_path : mbox->path);
		gkrellmd_send_to_client(client, line);
		g_free(line);
		}
	}

static GkrellmdMonitor mail_monitor =
	{
	"mail",
	update_mail,
	serve_mail_data,
	serve_mail_setup,
	NULL
	};

GkrellmdMonitor *
gkrellmd_init_mail_monitor(void)
	{
	gkrellmd_add_serveflag_done(&mail_need_serve);
	return &mail_monitor;
	}

#else	/* defined(WIN32) */

GkrellmdMonitor *
gkrellmd_init_mail_monitor(void)
	{
	return NULL;
	}

void
gkrellmd_add_mailbox(gchar *path)
    {
    }

#endif


void
gkrellm_mail_local_unsupported(void)
	{
	/* WIN32 only calls this and it is taken care of by above #if */
	}

GThread *
gkrellm_mail_get_active_thread(void)
	{
	return NULL;
	}

