/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/* camel-movemail.c: mbox copying function */

/*
 * Author:
 *  Dan Winship <danw@ximian.com>
 *
 * Copyright 2000 Ximian, Inc. (www.ximian.com)
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of version 2 of the GNU General Public
 * License 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 * USA
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <sys/stat.h>
#include <sys/uio.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#ifdef HAVE_ALLOCA_H
#include <alloca.h>
#endif

#include "camel-movemail.h"
#include "camel-exception.h"

#include "camel-mime-parser.h"
#include "camel-mime-filter.h"
#include "camel-mime-filter-from.h"

#include "camel-lock-client.h"

#define d(x)

#ifdef MOVEMAIL_PATH
#include <sys/wait.h>

static void movemail_external (const char *source, const char *dest,
			       CamelException *ex);
#endif

#ifdef HAVE_BROKEN_SPOOL
static int camel_movemail_copy_filter(int fromfd, int tofd, off_t start, size_t bytes, CamelMimeFilter *filter);
static int camel_movemail_solaris (int oldsfd, int dfd, CamelException *ex);
#else
/* these could probably be exposed as a utility? (but only mbox needs it) */
static int camel_movemail_copy_file(int sfd, int dfd, CamelException *ex);
#endif

#if 0
static int camel_movemail_copy(int fromfd, int tofd, off_t start, size_t bytes);
#endif

/**
 * camel_movemail: Copy an mbox file from a shared spool directory to a
 * new folder in a Camel store
 * @source: source file
 * @dest: destination file
 * @ex: a CamelException
 *
 * This copies an mbox file from a shared directory with multiple
 * readers and writers into a private (presumably Camel-controlled)
 * directory. Dot locking is used on the source file (but not the
 * destination).
 *
 * Return Value: Returns -1 on error.
 **/
int
camel_movemail(const char *source, const char *dest, CamelException *ex)
{
	int lockid = -1;
	int res = -1;
	int sfd, dfd;
	struct stat st;

	camel_exception_clear(ex);

	/* Stat and then open the spool file. If it doesn't exist or
	 * is empty, the user has no mail. (There's technically a race
	 * condition here in that an MDA might have just now locked it
	 * to deliver a message, but we don't care. In that case,
	 * assuming it's unlocked is equivalent to pretending we were
	 * called a fraction earlier.)
	 */
	if (stat (source, &st) == -1) {
		if (errno != ENOENT) {
			camel_exception_setv (ex, CAMEL_EXCEPTION_SYSTEM,
					      _("Could not check mail file %s: %s"),
					      source, g_strerror (errno));
		}
		return -1;
	}

	if (st.st_size == 0)
		return 0;

	/* open files */
	sfd = open (source, O_RDWR);
	if (sfd == -1) {
		camel_exception_setv (ex, CAMEL_EXCEPTION_SYSTEM,
				      _("Could not open mail file %s: %s"),
				      source, g_strerror (errno));
		return -1;
	}

	dfd = open (dest, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR);
	if (dfd == -1) {
		camel_exception_setv (ex, CAMEL_EXCEPTION_SYSTEM,
				      _("Could not open temporary mail "
					"file %s: %s"), dest,
				      g_strerror (errno));
		close (sfd);
		return -1;
	}

	/* lock our source mailbox */
	lockid = camel_lock_helper_lock(source, ex);
	if (lockid == -1) {
		close(sfd);
		close(dfd);
		return -1;
	}

#ifdef HAVE_BROKEN_SPOOL
	res = camel_movemail_solaris(sfd, dfd, ex);
#else
	res = camel_movemail_copy_file(sfd, dfd, ex);
#endif

	/* If no errors occurred copying the data, and we successfully
	 * close the destination file, then truncate the source file.
	 */
	if (res != -1) {
		if (close (dfd) == 0) {
			ftruncate (sfd, 0);
		} else {
			camel_exception_setv (ex, CAMEL_EXCEPTION_SYSTEM,
					      _("Failed to store mail in temp file %s: %s"),
					      dest, g_strerror (errno));
			res = -1;
		}
	} else
		close (dfd);
	close (sfd);

	camel_lock_helper_unlock(lockid);

	return res;
}

#ifdef MOVEMAIL_PATH
static void
movemail_external (const char *source, const char *dest, CamelException *ex)
{
	sigset_t mask, omask;
	pid_t pid;
	int fd[2], len = 0, nread, status;
	char buf[BUFSIZ], *output = NULL;

	/* Block SIGCHLD so the app can't mess us up. */
	sigemptyset (&mask);
	sigaddset (&mask, SIGCHLD);
	sigprocmask (SIG_BLOCK, &mask, &omask);

	if (pipe (fd) == -1) {
		sigprocmask (SIG_SETMASK, &omask, NULL);
		camel_exception_setv (ex, CAMEL_EXCEPTION_SYSTEM,
				      _("Could not create pipe: %s"),
				      g_strerror (errno));
		return;
	}

	pid = fork ();
	switch (pid) {
	case -1:
		close (fd[0]);
		close (fd[1]);
		sigprocmask (SIG_SETMASK, &omask, NULL);
		camel_exception_setv (ex, CAMEL_EXCEPTION_SYSTEM,
				      _("Could not fork: %s"),
				      g_strerror (errno));
		return;

	case 0:
		/* Child */
		close (fd[0]);
		close (STDIN_FILENO);
		dup2 (fd[1], STDOUT_FILENO);
		dup2 (fd[1], STDERR_FILENO);

		execl (MOVEMAIL_PATH, MOVEMAIL_PATH, source, dest, NULL);
		_exit (255);
		break;

	default:
		break;
	}

	/* Parent */
	close (fd[1]);

	/* Read movemail's output. */
	while ((nread = read (fd[0], buf, sizeof (buf))) > 0) {
		output = g_realloc (output, len + nread + 1);
		memcpy (output + len, buf, nread);
		len += nread;
		output[len] = '\0';
	}
	close (fd[0]);

	/* Now get the exit status. */
	while (waitpid (pid, &status, 0) == -1 && errno == EINTR)
		;
	sigprocmask (SIG_SETMASK, &omask, NULL);

	if (!WIFEXITED (status) || WEXITSTATUS (status) != 0) {
		camel_exception_setv (ex, CAMEL_EXCEPTION_SYSTEM,
				      _("Movemail program failed: %s"),
				      output ? output : _("(Unknown error)"));
	}
	g_free (output);
}
#endif

#ifndef HAVE_BROKEN_SPOOL
static int
camel_movemail_copy_file(int sfd, int dfd, CamelException *ex)
{
	int nread, nwrote;
	char buf[4096];

	while (1) {
		int written = 0;

		nread = read (sfd, buf, sizeof (buf));
		if (nread == 0)
			break;
		else if (nread == -1) {
			if (errno == EINTR)
				continue;
			camel_exception_setv (ex, CAMEL_EXCEPTION_SYSTEM,
					      _("Error reading mail file: %s"),
					      g_strerror (errno));
			return -1;
		}

		while (nread) {
			nwrote = write (dfd, buf + written, nread);
			if (nwrote == -1) {
				if (errno == EINTR)
					continue; /* continues inner loop */
				camel_exception_setv (ex, CAMEL_EXCEPTION_SYSTEM,
						      _("Error writing mail temp file: %s"),
						      g_strerror (errno));
				return -1;
			}
			written += nwrote;
			nread -= nwrote;
		}
	}

	return 0;
}
#endif

#if 0
static int
camel_movemail_copy(int fromfd, int tofd, off_t start, size_t bytes)
{
        char buffer[4096];
        int written = 0;

	d(printf("writing %d bytes ... ", bytes));

	if (lseek(fromfd, start, SEEK_SET) != start)
		return -1;

        while (bytes>0) {
                int toread, towrite;

                toread = bytes;
                if (bytes>4096)
                        toread = 4096;
                else
                        toread = bytes;
		do {
			towrite = read(fromfd, buffer, toread);
		} while (towrite == -1 && errno == EINTR);

		if (towrite == -1)
			return -1;

                /* check for 'end of file' */
                if (towrite == 0) {
			d(printf("end of file?\n"));
                        break;
		}

		do {
			toread = write(tofd, buffer, towrite);
		} while (toread == -1 && errno == EINTR);

		if (toread == -1)
			return -1;

                written += toread;
                bytes -= toread;
        }

        d(printf("written %d bytes\n", written));

        return written;
}
#endif

#define PRE_SIZE (32)

#ifdef HAVE_BROKEN_SPOOL
static int
camel_movemail_copy_filter(int fromfd, int tofd, off_t start, size_t bytes, CamelMimeFilter *filter)
{
        char buffer[4096+PRE_SIZE];
        int written = 0;
	char *filterbuffer;
	int filterlen, filterpre;

	d(printf("writing %d bytes ... ", bytes));

	camel_mime_filter_reset(filter);

	if (lseek(fromfd, start, SEEK_SET) != start)
		return -1;

        while (bytes>0) {
                int toread, towrite;

                toread = bytes;
                if (bytes>4096)
                        toread = 4096;
                else
                        toread = bytes;
		do {
			towrite = read(fromfd, buffer+PRE_SIZE, toread);
		} while (towrite == -1 && errno == EINTR);

		if (towrite == -1)
			return -1;

		d(printf("read %d unfiltered bytes\n", towrite));

                /* check for 'end of file' */
                if (towrite == 0) {
			d(printf("end of file?\n"));
			camel_mime_filter_complete(filter, buffer+PRE_SIZE, towrite, PRE_SIZE,
						   &filterbuffer, &filterlen, &filterpre);
			towrite = filterlen;
			if (towrite == 0)
				break;
		} else {
			camel_mime_filter_filter(filter, buffer+PRE_SIZE, towrite, PRE_SIZE,
						 &filterbuffer, &filterlen, &filterpre);
			towrite = filterlen;
		}

		d(printf("writing %d filtered bytes\n", towrite));

		do {
			toread = write(tofd, filterbuffer, towrite);
		} while (toread == -1 && errno == EINTR);

		if (toread == -1)
			return -1;

                written += toread;
                bytes -= toread;
        }

        d(printf("written %d bytes\n", written));

        return written;
}

/* write the headers back out again, but not he Content-Length header, because we dont
   want	to maintain it! */
static int
solaris_header_write(int fd, struct _camel_header_raw *header)
{
        struct iovec iv[4];
        int outlen = 0, len;

        iv[1].iov_base = ":";
        iv[1].iov_len = 1;
        iv[3].iov_base = "\n";
        iv[3].iov_len = 1;

        while (header) {
		if (strcasecmp(header->name, "Content-Length")) {
			iv[0].iov_base = header->name;
			iv[0].iov_len = strlen(header->name);
			iv[2].iov_base = header->value;
			iv[2].iov_len = strlen(header->value);
		
			do {
				len = writev(fd, iv, 4);
			} while (len == -1 && errno == EINTR);
			
			if (len == -1)
				return -1;
			outlen += len;
		}
                header = header->next;
        }

	do {
		len = write(fd, "\n", 1);
	} while (len == -1 && errno == EINTR);

	if (len == -1)
		return -1;

	outlen += 1;

	d(printf("Wrote %d bytes of headers\n", outlen));

        return outlen;
}

/* Well, since Solaris is a tad broken wrt its 'mbox' folder format,
   we must convert it to a real mbox format.  Thankfully this is
   mostly pretty easy */
static int
camel_movemail_solaris (int oldsfd, int dfd, CamelException *ex)
{
	CamelMimeParser *mp;
	char *buffer;
	int len;
	int sfd;
	CamelMimeFilterFrom *ffrom;
	int ret = 1;
	char *from = NULL;

	/* need to dup as the mime parser will close on finish */
	sfd = dup(oldsfd);
	if (sfd == -1) {
		camel_exception_setv (ex, CAMEL_EXCEPTION_SYSTEM,
				      _("Error copying mail temp file: %s"),
				      g_strerror (errno));
		return -1;
	}

	mp = camel_mime_parser_new();
	camel_mime_parser_scan_from(mp, TRUE);
	camel_mime_parser_init_with_fd(mp, sfd);

	ffrom = camel_mime_filter_from_new();

	while (camel_mime_parser_step(mp, &buffer, &len) == CAMEL_MIME_PARSER_STATE_FROM) {
		g_assert(camel_mime_parser_from_line(mp));
		from = g_strdup(camel_mime_parser_from_line(mp));
		if (camel_mime_parser_step(mp, &buffer, &len) != CAMEL_MIME_PARSER_STATE_FROM_END) {
			const char *cl;
			int length;
			int start, body;
			off_t newpos;

			ret = 0;

			start = camel_mime_parser_tell_start_from(mp);
			body = camel_mime_parser_tell(mp);

			if (write(dfd, from, strlen(from)) != strlen(from))
				goto fail;

			/* write out headers, but NOT content-length header */
			if (solaris_header_write(dfd, camel_mime_parser_headers_raw(mp)) == -1)
				goto fail;

			cl = camel_mime_parser_header(mp, "content-length", NULL);
			if (cl == NULL) {
				g_warning("Required Content-Length header is missing from solaris mail box @ %d", (int)camel_mime_parser_tell(mp));
				camel_mime_parser_drop_step(mp);
				camel_mime_parser_drop_step(mp);
				camel_mime_parser_step(mp, &buffer, &len);
				camel_mime_parser_unstep(mp);
				length = camel_mime_parser_tell_start_from(mp) - body;
				newpos = -1;
			} else {
				length = atoi(cl);
				camel_mime_parser_drop_step(mp);
				camel_mime_parser_drop_step(mp);
				newpos = length+body;
			}
			/* copy body->length converting From lines */
			if (camel_movemail_copy_filter(sfd, dfd, body, length, (CamelMimeFilter *)ffrom) == -1)
				goto fail;
			if (newpos != -1)
				camel_mime_parser_seek(mp, newpos, SEEK_SET);
		} else {
			g_error("Inalid parser state: %d", camel_mime_parser_state(mp));
		}
		g_free(from);
	}

	camel_object_unref((CamelObject *)mp);
	camel_object_unref((CamelObject *)ffrom);

	return ret;

fail:
	g_free(from);

	camel_exception_setv (ex, CAMEL_EXCEPTION_SYSTEM,
			      _("Error copying mail temp file: %s"),
			      g_strerror (errno));


	camel_object_unref((CamelObject *)mp);
	camel_object_unref((CamelObject *)ffrom);

	return -1;
}
#endif /* HAVE_BROKEN_SPOOL */