/*
 Copyright (C) 1999-2004 IC & S  dbmail@ic-s.nl

 This program 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 2 of the License, or (at your option) any later 
 version.

 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., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

/* $Id: pipe.c 1690 2005-03-18 12:48:34Z paul $
 *
 * Functions for reading the pipe from the MTA */


#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "db.h"
#include "auth.h"
#include "debug.h"
#include "list.h"
#include "forward.h"
#include "sort.h"
#include "dbmail.h"
#include "pipe.h"
#include "debug.h"
#include "misc.h"
#include <errno.h>
#include <string.h>
#include <strings.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include "dbmd5.h"
#include "misc.h"
#include "dsn.h"

#define HEADER_BLOCK_SIZE 1024
#define QUERY_SIZE 255
#define MAX_U64_STRINGSIZE 40
#define MAX_COMM_SIZE 512
#define RING_SIZE 6

#define AUTO_NOTIFY_SENDER "autonotify@dbmail"
#define AUTO_NOTIFY_SUBJECT "NEW MAIL NOTIFICATION"

/* Must be at least 998 or 1000 by RFC's */
#define MAX_LINE_SIZE 1024

#define DBMAIL_TEMPMBOX "INBOX"

/**
 * store a messagebody (without headers in one or more blocks in the database
 * \param message the message
 * \param message_size size of message
 * \param msgidnr idnr of message
 * \return 
 *     - -1 on error
 *     -  1 on success
 */
static int store_message_in_blocks(const char* message,
				   u64_t message_size,
				   u64_t msgidnr);

/* 
 * Send an automatic notification using sendmail
 */
static int send_notification(const char *to, const char *from,
			     const char *subject)
{
	FILE *mailpipe = NULL;
	char *sendmail_command = NULL;
	field_t sendmail;
	int result;
	size_t sendmail_command_maxlen;

	if (GetConfigValue("SENDMAIL", "SMTP", sendmail) < 0) 
		trace(TRACE_FATAL,
		      "%s,%s: error getting Config Values",
		      __FILE__, __func__);

	if (sendmail[0] == '\0')
		trace(TRACE_FATAL,
		      "send_notification(): SENDMAIL not configured (see config file). Stop.");

	trace(TRACE_DEBUG,
	      "send_notification(): found sendmail command to be [%s]",
	      sendmail);
	
	sendmail_command_maxlen = strlen((char *) to) + strlen(sendmail) + 2;

	sendmail_command = (char *) dm_malloc(sendmail_command_maxlen *
					      sizeof(char));
	if (!sendmail_command) {
		trace(TRACE_ERROR, "send_notification(): out of memory");
		return -1;
	}

	trace(TRACE_DEBUG, "send_notification(): allocated memory for"
	      " external command call");
	(void) snprintf(sendmail_command, sendmail_command_maxlen,
		"%s %s", sendmail, to);

	trace(TRACE_INFO, "send_notification(): opening pipe to command "
	      "%s", sendmail_command);


	if (!(mailpipe = popen(sendmail_command, "w"))) {
		trace(TRACE_ERROR,
		      "send_notification(): could not open pipe to sendmail using cmd [%s]",
		      sendmail);
		dm_free(sendmail_command);
		return 1;
	}

	trace(TRACE_DEBUG,
	      "send_notification(): pipe opened, sending data");

	fprintf(mailpipe, "To: %s\n", to);
	fprintf(mailpipe, "From: %s\n", from);
	fprintf(mailpipe, "Subject: %s\n", subject);
	fprintf(mailpipe, "\n");

	result = pclose(mailpipe);
	trace(TRACE_DEBUG, "send_notification(): pipe closed");

	if (result != 0)
		trace(TRACE_ERROR,
		      "send_notification(): reply could not be sent: sendmail error");
	dm_free(sendmail_command);
	return 0;
}


/*
 * Send an automatic reply using sendmail
 */
static int send_reply(struct list *headerfields, const char *body)
{
	struct element *el;
	struct mime_record *record;
	char *from = NULL, *to = NULL, *replyto = NULL, *subject = NULL;
	FILE *mailpipe = NULL;
	char *send_address;
	char *escaped_send_address;
	char comm[MAX_COMM_SIZE];
			    /**< command sent to sendmail (needs to escaped) */
	field_t sendmail;
	int result;
	unsigned int i, j;

	if (GetConfigValue("SENDMAIL", "SMTP", sendmail) < 0)
		trace(TRACE_FATAL,
		      "%s,%s: error getting config",
		      __FILE__, __func__);


	if (sendmail[0] == '\0')
		trace(TRACE_FATAL,
		      "send_reply(): SENDMAIL not configured (see config file). Stop.");

	trace(TRACE_DEBUG,
	      "send_reply(): found sendmail command to be [%s]", sendmail);

	/* find To: and Reply-To:/From: field */
	el = list_getstart(headerfields);

	while (el) {
		record = (struct mime_record *) el->data;

		if (strcasecmp(record->field, "from") == 0) {
			from = record->value;
			trace(TRACE_DEBUG, "send_reply(): found FROM [%s]",
			      from);
		} else if (strcasecmp(record->field, "reply-to") == 0) {
			replyto = record->value;
			trace(TRACE_DEBUG,
			      "send_reply(): found REPLY-TO [%s]",
			      replyto);
		} else if (strcasecmp(record->field, "subject") == 0) {
			subject = record->value;
			trace(TRACE_DEBUG,
			      "send_reply(): found SUBJECT [%s]", subject);
		} else if (strcasecmp(record->field, "deliver-to") == 0) {
			to = record->value;
			trace(TRACE_DEBUG, "send_reply(): found TO [%s]",
			      to);
		}

		el = el->nextnode;
	}

	if (!from && !replyto) {
		trace(TRACE_ERROR, "send_reply(): no address to send to");
		return 0;
	}


	trace(TRACE_DEBUG,
	      "send_reply(): header fields scanned; opening pipe to sendmail");
	send_address = replyto ? replyto : from;
	/* allocate a string twice the size of send_address */
	escaped_send_address =
		(char *) dm_malloc((strlen(send_address) + 1)
				   * 2 * sizeof(char));
	if (!escaped_send_address) {
		trace(TRACE_ERROR, "%s,%s: unable to allocate memory. Memory "
		      "full?", __FILE__, __func__);
		return 0;
	}
	memset(escaped_send_address, '\0', (strlen(send_address) + 1) * 2 * sizeof(char));
	i = 0;
	j = 0;
	/* get all characters from send_address, and escape every ' */
	while (i < (strlen(send_address) + 1)) {
		if (send_address[i] == '\'')
			escaped_send_address[j++] = '\\';
		escaped_send_address[j++] = send_address[i++];
	}
	(void) snprintf(comm, MAX_COMM_SIZE, "%s '%s'", sendmail,
		 escaped_send_address);

	if (!(mailpipe = popen(comm, "w"))) {
		trace(TRACE_ERROR,
		      "send_reply(): could not open pipe to sendmail using cmd [%s]",
		      comm);
		dm_free(escaped_send_address);
		return 1;
	}

	trace(TRACE_DEBUG, "send_reply(): sending data");

	fprintf(mailpipe, "To: %s\n", replyto ? replyto : from);
	fprintf(mailpipe, "From: %s\n", to ? to : "(unknown)");
	fprintf(mailpipe, "Subject: AW: %s\n",
		subject ? subject : "<no subject>");
	fprintf(mailpipe, "\n");
	fprintf(mailpipe, "%s\n", body ? body : "--");

	result = pclose(mailpipe);
	trace(TRACE_DEBUG, "send_reply(): pipe closed");
	if (result != 0)
		trace(TRACE_ERROR,
		      "send_reply(): reply could not be sent: sendmail error");
	dm_free(escaped_send_address);
	return 0;
}


/* Yeah, RAN. That's Reply And Notify ;-) */
static int execute_auto_ran(u64_t useridnr, struct list *headerfields)
{
	field_t val;
	int do_auto_notify = 0, do_auto_reply = 0;
	char *reply_body = NULL;
	char *notify_address = NULL;

	/* message has been succesfully inserted, perform auto-notification & auto-reply */
	if (GetConfigValue("AUTO_NOTIFY", "SMTP", val) < 0)
		trace(TRACE_FATAL, "%s,%s error getting config",
		      __FILE__, __func__);

	if (strcasecmp(val, "yes") == 0)
		do_auto_notify = 1;

	if (GetConfigValue("AUTO_REPLY", "SMTP", val) < 0)
		trace(TRACE_FATAL, "%s,%s error getting config",
		      __FILE__, __func__);

	if (strcasecmp(val, "yes") == 0)
		do_auto_reply = 1;

	if (do_auto_notify != 0) {
		trace(TRACE_DEBUG,
		      "execute_auto_ran(): starting auto-notification procedure");

		if (db_get_notify_address(useridnr, &notify_address) != 0)
			trace(TRACE_ERROR,
			      "execute_auto_ran(): error fetching notification address");
		else {
			if (notify_address == NULL)
				trace(TRACE_DEBUG,
				      "execute_auto_ran(): no notification address specified, skipping");
			else {
				trace(TRACE_DEBUG,
				      "execute_auto_ran(): sending notifcation to [%s]",
				      notify_address);
				if (send_notification(notify_address,
						      AUTO_NOTIFY_SENDER,
						      AUTO_NOTIFY_SUBJECT) < 0) {
					trace(TRACE_ERROR, "%s,%s: error in call to send_notification.",
					      __FILE__, __func__);
					dm_free(notify_address);
					return -1;
				}
				dm_free(notify_address);
			}
		}
	}

	if (do_auto_reply != 0) {
		trace(TRACE_DEBUG,
		      "execute_auto_ran(): starting auto-reply procedure");

		if (db_get_reply_body(useridnr, &reply_body) != 0)
			trace(TRACE_ERROR,
			      "execute_auto_ran(): error fetching reply body");
		else {
			if (reply_body == NULL || reply_body[0] == '\0')
				trace(TRACE_DEBUG,
				      "execute_auto_ran(): no reply body specified, skipping");
			else {
				if (send_reply(headerfields, reply_body) < 0) {
					trace(TRACE_ERROR, "%s,%s: error in call to send_reply",
					      __FILE__, __func__);
					dm_free(reply_body);
					return -1;
				}
				dm_free(reply_body);
				
			}
		}
	}

	return 0;
}


/* read from instream, but simply discard all input! */
int discard_client_input(FILE * instream)
{
	char *tmpline;

	tmpline = (char *) dm_malloc(MAX_LINE_SIZE + 1);
	if (tmpline == NULL) {
		trace(TRACE_ERROR, "%s,%s: unable to allocate memory.",
		      __FILE__, __func__);
		return -1;
	}
	
	while (!feof(instream)) {
		if (fgets(tmpline, MAX_LINE_SIZE, instream) == NULL)
			break;

		trace(TRACE_DEBUG, "%s,%s: tmpline = [%s]", __FILE__,
		      __func__, tmpline);
		if (strcmp(tmpline, ".\r\n") == 0)
			break;
	}
	dm_free(tmpline);
	return 0;
}

/**
 * store a temporary copy of a message.
 * \param header the header to the message
 * \param body body of the message
 * \param headersize size of header
 * \param headerrfcsize rfc size of header
 * \param bodysize size of body
 * \param bodyrfcsize rfc size of body
 * \param[out] temp_message_idnr message idnr of temporary message
 * \return 
 *     - -1 on error
 *     -  1 on success
 */
static int store_message_temp(const char *header, const char *body, 
			      u64_t headersize, u64_t headerrfcsize,
			      u64_t bodysize, u64_t bodyrfcsize,
			      /*@out@*/ u64_t * temp_message_idnr)
{
	int result;
	u64_t user_idnr;
	u64_t msgidnr;
	u64_t messageblk_idnr;
	char unique_id[UID_SIZE];
	
	result = auth_user_exists(DBMAIL_DELIVERY_USERNAME, &user_idnr);
	if (result < 0) {
		trace(TRACE_ERROR,
		      "%s,%s: unable to find user_idnr for user " "[%s]\n",
		      __FILE__, __func__, DBMAIL_DELIVERY_USERNAME);
		return -1;
	}
	if (result == 0) {
		trace(TRACE_ERROR,
		      "%s,%s: unable to find user_idnr for user "
		      "[%s]. Make sure this system user is in the database!\n",
		      __FILE__, __func__, DBMAIL_DELIVERY_USERNAME);
		return -1;
	}
	create_unique_id(unique_id, user_idnr);
	/* create a message record */
	switch (db_insert_message(user_idnr, DBMAIL_TEMPMBOX,
				  CREATE_IF_MBOX_NOT_FOUND, unique_id,
				  &msgidnr)) {
	case -1:
		trace(TRACE_ERROR,
		      "store_message_temp(): returned -1, aborting");
		return -1;
	}

	switch (db_insert_message_block
		(header, headersize, msgidnr, &messageblk_idnr,HEAD_BLOCK)) {
	case -1:
		trace(TRACE_ERROR,
		      "store_message_temp(): error inserting msgblock [header]");
		return -1;
	}
	trace(TRACE_DEBUG,
	      "store_message_temp(): allocating [%ld] bytes of memory "
	      "for readblock", READ_BLOCK_SIZE);
	
	/* store body in several blocks (if needed */
	if (store_message_in_blocks(body, bodysize, msgidnr) < 0) {
		trace(TRACE_STOP,
		      "store_message_temp(): db_insert_message_block "
		      "failed");
		return -1;
	}

	if (db_update_message(msgidnr, unique_id, (headersize + bodysize),
			      (headerrfcsize + bodyrfcsize)) < 0) {
		trace(TRACE_ERROR, "%s,%s: error updating message [%llu]. "
		      "Trying to clean up", __FILE__, __func__, msgidnr);
		if (db_delete_message(msgidnr) < 0) 
			trace(TRACE_ERROR, "%s,%s error deleting message "
			      "[%llu]. Database might be inconsistent, run "
			      "dbmail-util", __FILE__, __func__,
			      msgidnr);
		return -1;
	}

	*temp_message_idnr = msgidnr;
	return 1;
}

int store_message_in_blocks(const char *message, u64_t message_size,
			    u64_t msgidnr) 
{
	u64_t tmp_messageblk_idnr;
	u64_t rest_size = message_size;
	u64_t block_size = 0;
	unsigned block_nr = 0;
	size_t offset;

	while (rest_size > 0) {
		offset = block_nr * READ_BLOCK_SIZE;
		block_size = (rest_size < READ_BLOCK_SIZE ? 
			      rest_size : READ_BLOCK_SIZE);
		rest_size = (rest_size < READ_BLOCK_SIZE ?
			     0 : rest_size - READ_BLOCK_SIZE);
		trace(TRACE_DEBUG, "%s,%s: inserting message: %s",
		      __FILE__, __func__, &message[offset]);
		if (db_insert_message_block(&message[offset],
					    block_size, msgidnr,
					    &tmp_messageblk_idnr,BODY_BLOCK) < 0) {
			trace(TRACE_ERROR, "%s,%s: "
			      "db_insert_message_block() failed",
			      __FILE__, __func__);
			return -1;
		}
		
			
		block_nr += 1;
	}

	return 1;
}

/* Here's the real *meat* of this source file!
 *
 * Function: insert_messages()
 * What we get:
 *   - A pointer to the incoming message stream
 *   - The header of the message 
 *   - A list of destination addresses / useridnr's
 *   - The default mailbox to delivery to
 *
 * What we do:
 *   - Read in the rest of the message
 *   - Store the message to the DBMAIL user
 *   - Process the destination addresses into lists:
 *     - Local useridnr's
 *     - External forwards
 *     - No such user bounces
 *   - Store the local useridnr's
 *     - Run the message through each user's sorting rules
 *     - Potentially alter the delivery:
 *       - Different mailbox
 *       - Bounce
 *       - Reply with vacation message
 *       - Forward to another address
 *     - Check the user's quota before delivering
 *       - Do this *after* their sorting rules, since the
 *         sorting rules might not store the message anyways
 *   - Send out the no such user bounces
 *   - Send out the external forwards
 *   - Delete the temporary message from the database
 * What we return:
 *   - 0 on success
 *   - -1 on full failure
 */
int insert_messages(const char *header, const char* body, u64_t headersize,
		    u64_t headerrfcsize, u64_t bodysize, u64_t bodyrfcsize,
		    struct list *headerfields,
		    struct list *dsnusers, struct list *returnpath)
{
	struct element *element, *ret_path;
	u64_t msgsize, rfcsize, tmpmsgidnr;

	delivery_status_t final_dsn;
	
	msgsize = headersize + bodysize;
	rfcsize = headerrfcsize + bodyrfcsize;

	/* first start a new database transaction */
        if (db_begin_transaction() < 0) {
                trace(TRACE_ERROR, "%s,%s: error executing "
			"db_begin_transaction(). aborting delivery...",
			__FILE__, __func__);
		return -1;
	}

		
	/* Read in the rest of the stream and store it into a temporary message */
	switch (store_message_temp
		(header, body, headersize, headerrfcsize, 
		 bodysize, bodyrfcsize, &tmpmsgidnr)) {
	case -1:
		/* Major trouble. Bail out immediately. */
		trace(TRACE_ERROR,
		      "%s, %s: failed to store temporary message.",
		      __FILE__, __func__);
		db_rollback_transaction();
		return -1;
	default:
		trace(TRACE_DEBUG, "%s, %s: temporary msgidnr is [%llu]",
		      __FILE__, __func__, tmpmsgidnr);
		break;
	}

	/* Loop through the users list. */
	for (element = list_getstart(dsnusers); element != NULL; element = element->nextnode) {
		struct element *userid_elem;
		int has_2 = 0, has_4 = 0, has_5 = 0, has_5_2 = 0;
		deliver_to_user_t *delivery = (deliver_to_user_t *) element->data;
		dsn_class_t dsn_result;

		/* If there was already an error during resolving,
		 * let's skip this delivery. */
		if (delivery->dsn.class != DSN_CLASS_OK)
			continue;
		
		/* Each user may have a list of user_idnr's for local
		 * delivery. */
		for (userid_elem = list_getstart(delivery->userids);
		     userid_elem != NULL;
		     userid_elem = userid_elem->nextnode) {
			u64_t useridnr = *(u64_t *) userid_elem->data;
			
			trace(TRACE_DEBUG,
			      "%s, %s: calling sort_and_deliver for useridnr [%llu]",
			      __FILE__, __func__, useridnr);
			
			dsn_result = sort_and_deliver(tmpmsgidnr, msgsize, useridnr, delivery->mailbox);
			
			switch (dsn_result) {
			case DSN_CLASS_OK:
				/* Indicate success. */
				trace(TRACE_DEBUG,
				      "%s, %s: successful sort_and_deliver for useridnr [%llu]",
				      __FILE__, __func__, useridnr);
				has_2 = 1;
				break;
			case DSN_CLASS_FAIL:
				/* Indicate permanent failure. */
				trace(TRACE_ERROR,
				      "%s, %s: permanent failure sort_and_deliver for useridnr [%llu]",
				      __FILE__, __func__, useridnr);
				has_5 = 1;
				break;
			case DSN_CLASS_QUOTA:
				/* Indicate over quota. */
				trace(TRACE_ERROR,
				      "%s, %s: temporary failure sort_and_deliver for useridnr [%llu]",
				      __FILE__, __func__, useridnr);
				has_5_2 = 1;
				break;
			case DSN_CLASS_TEMP:
			default:
				/* Assume a temporary failure */
				trace(TRACE_ERROR,
				      "%s, %s: temporary failure sort_and_deliver for useridnr [%llu]",
				      __FILE__, __func__, useridnr);
				has_4 = 1;
				break;
			}

			/* Automatic reply and notification */
			if (execute_auto_ran(useridnr, headerfields) < 0)
				trace(TRACE_ERROR, "%s,%s: error in "
				      "execute_auto_ran(), continuing",
				      __FILE__, __func__);
		}		/* from: the useridnr for loop */

		final_dsn = dsnuser_worstcase_int(has_2, has_4, has_5, has_5_2);
		switch (final_dsn.class) {
		case DSN_CLASS_OK:
			delivery->dsn.class = DSN_CLASS_OK;	/* Success. */
			delivery->dsn.subject = 1;	/* Address related. */
			delivery->dsn.detail = 5;	/* Valid. */
			break;
		case DSN_CLASS_TEMP:
			/* sort_and_deliver returns TEMP is useridnr is 0, aka,
			 * if nothing was delivered at all, or for any other failures. */	

			/* If there's a problem with the delivery address, but
			 * there are proper forwarding addresses, we're OK. */
			if (list_totalnodes(delivery->forwards) > 0) {
				delivery->dsn.class = DSN_CLASS_OK;
				delivery->dsn.subject = 1;	/* Address related. */
				delivery->dsn.detail = 5;	/* Valid. */
				break;
			}
			/* Fall through to FAIL. */
		case DSN_CLASS_FAIL:
			delivery->dsn.class = DSN_CLASS_FAIL;	/* Permanent failure. */
			delivery->dsn.subject = 1;	/* Address related. */
			delivery->dsn.detail = 1;	/* Does not exist. */
			break;
		case DSN_CLASS_QUOTA:
			delivery->dsn.class = DSN_CLASS_FAIL;	/* Permanent failure. */
			delivery->dsn.subject = 2;	/* Mailbox related. */
			delivery->dsn.detail = 2;	/* Over quota limit. */
			break;
		case DSN_CLASS_NONE:
			/* Leave the DSN status at whatever dsnuser_resolve set it at. */
			break;
		}

		trace(TRACE_DEBUG,
		      "insert_messages(): we need to deliver [%ld] "
		      "messages to external addresses",
		      list_totalnodes(delivery->forwards));

		/* Each user may also have a list of external forwarding addresses. */
		if (list_totalnodes(delivery->forwards) > 0) {

			trace(TRACE_DEBUG,
			      "insert_messages(): delivering to external addresses");

			/* Only the last step of the returnpath is used. */
			ret_path = list_getstart(returnpath);

			/* Forward using the temporary stored message. */
			if (forward(tmpmsgidnr, delivery->forwards,
				(ret_path ? ret_path->
				 data : "DBMAIL-MAILER"), header,
				headersize) < 0)
				/* FIXME: if forward fails, we should do something 
				 * sensible. Currently, the message is just black-
				 * holed! */
				trace(TRACE_ERROR, "%s,%s: forward failed "
				      "message lost", __FILE__, __func__);
		}
	}			/* from: the delivery for loop */

	/* Always delete the temporary message, even if the delivery failed.
	 * It is the MTA's job to requeue or bounce the message,
	 * and our job to keep a tidy database ;-) */
	if (db_delete_message(tmpmsgidnr) < 0) 
		trace(TRACE_ERROR, "%s,%s: failed to delete temporary message "
		      "[%llu]", __FILE__, __func__, tmpmsgidnr);
	trace(TRACE_DEBUG,
	      "insert_messages(): temporary message deleted from database");

	trace(TRACE_DEBUG, "insert_messages(): End of function");

	if (db_commit_transaction() < 0)
		return -1;

	return 0;
}


syntax highlighted by Code2HTML, v. 0.9.1