/* GNU Mailutils -- a suite of utilities for electronic mail
   Copyright (C) 1999, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008,
   2009, 2010 Free Software Foundation, Inc.

   GNU Mailutils 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, or (at your option)
   any later version.

   GNU Mailutils 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 GNU Mailutils.  If not, see <http://www.gnu.org/licenses/>. */

#include "mail.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

static int isfilename (const char *);
static int msg_to_pipe (const char *cmd, mu_message_t msg);


/* Additional message headers */
struct add_header
{
  int mode;
  char *name;
  char *value;
};

static mu_list_t add_header_list;

static int
seed_headers (void *item, void *data)
{
  struct add_header *hp = item;
  compose_env_t *env = data;

  compose_header_set (env, hp->name, hp->value, hp->mode);
  return 0;
}

static int
list_headers (void *item, void *data)
{
  struct add_header *hp = item;
  char *name = data;

  if (!name || strcmp (name, hp->name) == 0)
    {
      mu_printf ("%s: %s\n", hp->name, hp->value);
    }
  return 0;
}
    
static void
add_header (char *name, char *value, int mode)
{
  struct add_header *hp;
  
  if (!add_header_list)
    {
      int rc = mu_list_create (&add_header_list);
      if (rc)
	{
	  mu_error (_("Cannot create header list: %s"), mu_strerror (rc));
	  exit (1);
	}
    }

  hp = xmalloc (sizeof (*hp));
  hp->mode = mode;
  hp->name = name;
  hp->value = value;
  mu_list_append (add_header_list, hp);
}

void
send_append_header (char *text)
{
  char *p;
  size_t len;
  char *name;

  p = strchr (text, ':');
  if (!p)
    {
      mu_error (_("Invalid header: %s"), text);
      return;
    }
  len = p - text;
  name = xmalloc (len + 1);
  memcpy (name, text, len);
  name[len] = 0;
  for (p++; *p && mu_isspace (*p); p++)
    ;

  add_header (name, strdup (p), COMPOSE_APPEND);
}

void
send_append_header2 (char *name, char *value, int mode)
{
  add_header (strdup (name), strdup (value), mode);
}

int
mail_sendheader (int argc, char **argv)
{
  if (argc == 1)
    mu_list_do (add_header_list, list_headers, NULL);
  else if (argc == 2)
    {
      if (strchr (argv[1], ':'))
	send_append_header (argv[1]);
      else
	mu_list_do (add_header_list, list_headers, argv[1]);
    }
  else
    {
      size_t len = strlen (argv[1]);
      if (len > 0 && argv[1][len - 1] == ':') 
	argv[1][len - 1] = 0;
      add_header (strdup (argv[1]), strdup (argv[2]), COMPOSE_APPEND);
    }
  return 0;
}


/* Send-related commands */

static void
read_cc_bcc (compose_env_t *env)
{
  if (mailvar_get (NULL, "askcc", mailvar_type_boolean, 0) == 0)
    compose_header_set (env, MU_HEADER_CC,
			ml_readline_with_intr ("Cc: "), COMPOSE_REPLACE);
  if (mailvar_get (NULL, "askbcc", mailvar_type_boolean, 0) == 0)
    compose_header_set (env, MU_HEADER_BCC,
			ml_readline_with_intr ("Bcc: "), COMPOSE_REPLACE);
}

/*
 * m[ail] address...
 if address is starting with
 
    '/'        it is considered a file and the message is saveed to a file;
    '.'        it is considered to be a relative path;
    '|'        it is considered to be a pipe, and the message is written to
               there;
	       
 example:
 
   mail joe '| cat >/tmp/save'

 mail will be sent to joe and the message saved in /tmp/save. */

int
mail_send (int argc, char **argv)
{
  compose_env_t env;
  int status;
  int save_to = mu_isupper (argv[0][0]);
  compose_init (&env);

  if (argc < 2)
    compose_header_set (&env, MU_HEADER_TO, ml_readline_with_intr ("To: "),
			COMPOSE_REPLACE);
  else
    {
      while (--argc)
	{
	  char *p = *++argv;
	  if (isfilename (p))
	    {
	      env.outfiles = realloc (env.outfiles,
				      (env.nfiles + 1) *
				      sizeof (*(env.outfiles)));
	      if (env.outfiles)
		{
		  env.outfiles[env.nfiles] = p;
		  env.nfiles++;
		}
	    }
	  else
	    compose_header_set (&env, MU_HEADER_TO, p, COMPOSE_SINGLE_LINE);
	}
    }

  if (mailvar_get (NULL, "mailx", mailvar_type_boolean, 0))
    read_cc_bcc (&env);

  if (mailvar_get (NULL, "asksub", mailvar_type_boolean, 0) == 0)
    compose_header_set (&env, MU_HEADER_SUBJECT,
			ml_readline_with_intr ("Subject: "), COMPOSE_REPLACE);

  status = mail_send0 (&env, save_to);
  compose_destroy (&env);
  return status;
}

void
compose_init (compose_env_t * env)
{
  memset (env, 0, sizeof (*env));
  mu_list_do (add_header_list, seed_headers, env);
}

int
compose_header_set (compose_env_t * env, const char *name,
		    const char *value, int mode)
{
  int status;
  char *old_value;

  if (!value || value[0] == 0)
    return EINVAL;

  if (!env->header
      && (status = mu_header_create (&env->header, NULL, 0)) != 0)
    {
      mu_error (_("Cannot create header: %s"), mu_strerror (status));
      return status;
    }

  switch (mode)
    {
    case COMPOSE_REPLACE:
    case COMPOSE_APPEND:
      if (is_address_field (name)
	  && mailvar_get (NULL, "inplacealiases", mailvar_type_boolean, 0) == 0)
	{
	  char *exp = alias_expand (value);
	  status = mu_header_set_value (env->header, name, exp ? exp : value,
					mode);
	  free (exp);
	}
      else
	status = mu_header_set_value (env->header, name, value, mode);
      break;

    case COMPOSE_SINGLE_LINE:
      if (mu_header_aget_value (env->header, name, &old_value) == 0
	  && old_value[0])
	{
	  if (is_address_field (name)
	      && mailvar_get (NULL, "inplacealiases", mailvar_type_boolean, 0) == 0)
	    {
	      char *exp = alias_expand (value);
	      status = util_merge_addresses (&old_value, exp ? exp : value);
	      if (status == 0)
		status = mu_header_set_value (env->header, name, old_value, 1);
	      free (exp);
	    }
	  else
	    {
	      size_t size = strlen (old_value) + strlen (value) + 2;
	      char *p = realloc (old_value, size);
	      if (!p)
		status = ENOMEM;
	      else
		{
		  old_value = p;
		  strcat (old_value, ",");
		  strcat (old_value, value);
		  status = mu_header_set_value (env->header, name, old_value,
						1);
		}
	    }
	  free (old_value);
	}
      else
	status = compose_header_set (env, name, value, COMPOSE_REPLACE);
    }

  return status;
}

char *
compose_header_get (compose_env_t * env, char *name, char *defval)
{
  char *p;

  if (mu_header_aget_value (env->header, name, &p))
    p = defval;
  return p;
}

void
compose_destroy (compose_env_t *env)
{
  mu_header_destroy (&env->header);
  free (env->outfiles);
}

static int
fill_body (mu_message_t msg, mu_stream_t instr)
{
  int rc;
  mu_body_t body = NULL;
  mu_stream_t stream = NULL;
  mu_off_t n;
  
  rc = mu_message_get_body (msg, &body);
  if (rc)
    {
      mu_error (_("cannot get message body: %s"), mu_strerror (rc));
      return 1;
    }
  rc = mu_body_get_streamref (body, &stream);
  if (rc)
    {
      mu_error (_("cannot get body: %s"), mu_strerror (rc));
      return 1;
    }

  rc = mu_stream_copy (stream, instr, 0, &n);
  mu_stream_destroy (&stream);

  if (rc)
    {
      mu_error (_("cannot copy temporary stream: %s"), mu_strerror (rc));
      return 1;
    }
  
  if (n == 0)
    {
      if (mailvar_get (NULL, "nullbody", mailvar_type_boolean, 0) == 0)
	{
	  char *str;
	  if (mailvar_get (&str, "nullbodymsg", mailvar_type_string, 0) == 0)
	    mu_error ("%s\n", _(str));
	}
      else
	return 1;
    }

  return 0;
}

static int
save_dead_message (compose_env_t *env)
{
  if (mailvar_get (NULL, "save", mailvar_type_boolean, 0) == 0)
    {
      mu_stream_t dead_letter;
      int rc;
      const char *name = getenv ("DEAD");

      /* FIXME: Use MU_STREAM_APPEND if appenddeadletter, instead of the
	 stream manipulations below */
      rc = mu_file_stream_create (&dead_letter, name, MU_STREAM_WRITE);
      if (rc)
	{
	  mu_error (_("Cannot open file %s: %s"), name, strerror (rc));
	  return 1;
	}
      if (mailvar_get (NULL, "appenddeadletter",
		       mailvar_type_boolean, 0) == 0)
	mu_stream_seek (dead_letter, 0, MU_SEEK_END, NULL);
      else
	mu_stream_truncate (dead_letter, 0);

      mu_stream_seek (env->compstr, 0, MU_SEEK_SET, NULL);
      mu_stream_copy (dead_letter, env->compstr, 0, NULL);
      mu_stream_destroy (&dead_letter);
    }
  return 0;
}

static int
send_message (mu_message_t msg)
{
  char *sendmail;
  int status;
  
  if (mailvar_get (&sendmail, "sendmail", mailvar_type_string, 0) == 0)
    {
      if (sendmail[0] == '/')
	status = msg_to_pipe (sendmail, msg);
      else
	{
	  mu_mailer_t mailer;
	  
	  status = mu_mailer_create (&mailer, sendmail);
	  if (status == 0)
	    {
	      if (mailvar_get (NULL, "verbose", mailvar_type_boolean, 0) == 0)
		{
		  mu_debug_set_category_level (MU_DEBCAT_MAILER,
				      MU_DEBUG_LEVEL_UPTO (MU_DEBUG_PROT));
		}
	      status = mu_mailer_open (mailer, MU_STREAM_RDWR);
	      if (status == 0)
		{
		  status = mu_mailer_send_message (mailer, msg, NULL, NULL);
		  mu_mailer_close (mailer);
		}
	      else
		mu_error (_("Cannot open mailer: %s"), mu_strerror (status));
	      mu_mailer_destroy (&mailer);
	    }
	  else
	    mu_error (_("Cannot create mailer: %s"),
			mu_strerror (status));
	}
    }
  else
    {
      mu_error (_("Variable sendmail not set: no mailer"));
      status = ENOSYS;
    }
  return status;
}

/* mail_send0(): shared between mail_send() and mail_reply();

   If the variable "record" is set, the outgoing message is
   saved after being sent. If "save_to" argument is non-zero,
   the name of the save file is derived from "to" argument. Otherwise,
   it is taken from the value of "record" variable.

   sendmail

   contains a command, possibly with options, that mailx invokes to send
   mail. You must manually set the default for this environment variable
   by editing ROOTDIR/etc/mailx.rc to specify the mail agent of your
   choice. The default is sendmail, but it can be any command that takes
   addresses on the command line and message contents on standard input. */

int
mail_send0 (compose_env_t *env, int save_to)
{
  int done = 0;
  int rc;
  char *savefile = NULL;
  int int_cnt;
  char *escape;

  /* Prepare environment */
  rc = mu_temp_file_stream_create (&env->compstr, NULL, 0);
  if (rc)
    {
      mu_error (_("Cannot open temporary file: %s"), mu_strerror (rc));
      return 1;
    }

  ml_clear_interrupt ();
  int_cnt = 0;
  while (!done)
    {
      char *buf;
      buf = ml_readline (" \b");

      if (ml_got_interrupt ())
	{
	  if (mailvar_get (NULL, "ignore", mailvar_type_boolean, 0) == 0)
	    {
	      mu_printf ("@\n");
	    }
	  else
	    {
	      if (buf)
		free (buf);
	      if (++int_cnt == 2)
		break;
	      mu_error (_("\n(Interrupt -- one more to kill letter)"));
	    }
	  continue;
	}

      if (!buf)
	{
	  if (interactive 
	      && mailvar_get (NULL, "ignoreeof", mailvar_type_boolean, 0) == 0)
	    {
	      mu_error (mailvar_get (NULL, "dot", mailvar_type_boolean, 0) == 0 ?
			  _("Use \".\" to terminate letter.") :
			  _("Use \"~.\" to terminate letter."));
	      continue;
	    }
	  else
	    break;
	}

      int_cnt = 0;

      if (strcmp (buf, ".") == 0
	  && mailvar_get (NULL, "dot", mailvar_type_boolean, 0) == 0)
	done = 1;
      else if (mailvar_get (&escape, "escape", mailvar_type_string, 0) == 0
	       && buf[0] == escape[0])
	{
	  if (buf[1] == buf[0])
	    mu_stream_printf (env->compstr, "%s\n", buf + 1);
	  else if (buf[1] == '.')
	    done = 1;
	  else if (buf[1] == 'x')
	    {
	      int_cnt = 2;
	      done = 1;
	    }
	  else
	    {
	      struct mu_wordsplit ws;
	      int status;

	      if (mu_wordsplit (buf + 1, &ws, MU_WRDSF_DEFFLAGS) == 0)
		{
		  if (ws.ws_wordc > 0)
		    {
		      const struct mail_escape_entry *entry = 
			mail_find_escape (ws.ws_wordv[0]);

		      if (entry)
			status = (*entry->escfunc) (ws.ws_wordc, ws.ws_wordv,
						    env);
		      else
			mu_error (_("Unknown escape %s"), ws.ws_wordv[0]);
		    }
		  else
		    mu_error (_("Unfinished escape"));
		  mu_wordsplit_free (&ws);
		}
	      else
		{
		  mu_error (_("Cannot parse escape sequence: %s"),
			      mu_wordsplit_strerror (&ws));
		}
	    }
	}
      else
	mu_stream_printf (env->compstr, "%s\n", buf);
      mu_stream_flush (env->compstr);
      free (buf);
    }

  /* If interrupted, dump the file to dead.letter.  */
  if (int_cnt)
    {
      save_dead_message (env);
      mu_stream_destroy (&env->compstr);
      return 1;
    }

  /* In mailx compatibility mode, ask for Cc and Bcc after editing
     the body of the message */
  if (mailvar_get (NULL, "mailx", mailvar_type_boolean, 0) == 0)
    read_cc_bcc (env);

  /* Prepare the header */
  if (mailvar_get (NULL, "xmailer", mailvar_type_boolean, 0) == 0)
    mu_header_set_value (env->header, MU_HEADER_X_MAILER, 
                         program_version, 1);

  if (util_header_expand (&env->header) == 0)
    {
      mu_message_t msg = NULL;
      int rc;
      int status = 0;
      
      mu_message_create (&msg, NULL);
      
      /* Fill the body.  */
      mu_stream_seek (env->compstr, 0, MU_SEEK_SET, NULL);
      rc = fill_body (msg, env->compstr);

      if (rc == 0)
	{
	  /* Save outgoing message */
	  if (save_to)
	    {
	      char *tmp = compose_header_get (env, MU_HEADER_TO, NULL);
	      mu_address_t addr = NULL;
	      
	      mu_address_create (&addr, tmp);
	      mu_address_aget_email (addr, 1, &savefile);
	      mu_address_destroy (&addr);
	      if (savefile)
		{
		  char *p = strchr (savefile, '@');
		  if (p)
		    *p = 0;
		}
	    }
	  util_save_outgoing (msg, savefile);
	  if (savefile)
	    free (savefile);

	  /* Check if we need to save the message to files or pipes.  */
	  if (env->outfiles)
	    {
	      int i;
	      for (i = 0; i < env->nfiles; i++)
		{
		  /* Pipe to a cmd.  */
		  if (env->outfiles[i][0] == '|')
		    status = msg_to_pipe (env->outfiles[i] + 1, msg);
		  /* Save to a file.  */
		  else
		    {
		      mu_mailbox_t mbx = NULL;
		      status = mu_mailbox_create_default (&mbx, 
							  env->outfiles[i]);
		      if (status == 0)
			{
			  status = mu_mailbox_open (mbx, MU_STREAM_WRITE
						    | MU_STREAM_CREAT);
			  if (status == 0)
			    {
			      status = mu_mailbox_append_message (mbx, msg);
			      if (status)
				mu_error (_("Cannot append message: %s"),
					    mu_strerror (status));
			      mu_mailbox_close (mbx);
			    }
			  mu_mailbox_destroy (&mbx);
			}
		      if (status)
			mu_error (_("Cannot create mailbox %s: %s"), 
				    env->outfiles[i],
				    mu_strerror (status));
		    }
		}
	    }
	  
	  /* Do we need to Send the message on the wire?  */
	  if (status == 0 &&
	      (compose_header_get (env, MU_HEADER_TO, NULL) ||
	       compose_header_get (env, MU_HEADER_CC, NULL) ||
	       compose_header_get (env, MU_HEADER_BCC, NULL)))
	    {
	      mu_message_set_header (msg, env->header, NULL);
	      env->header = NULL;
	      status = send_message (msg);
	      if (status)
		{
		  mu_error (_("cannot send message: %s"),
			    mu_strerror (status));
		  save_dead_message (env);
		}
	    }
	}  
      mu_stream_destroy (&env->compstr);
      mu_message_destroy (&msg, NULL);
      return status;
    }
  else
    save_dead_message (env);
  
  mu_stream_destroy (&env->compstr);
  return 1;
}

/* Starting with '|' '/' or not consider addresses and we cheat
   by adding '.' in the mix for none absolute path.  */
static int
isfilename (const char *p)
{
  if (p)
    if (*p == '/' || *p == '.' || *p == '|')
      return 1;
  return 0;
}

/* FIXME: Should probably be in util.c.  */
/* Call popen(cmd) and write the message to it.  */
static int
msg_to_pipe (const char *cmd, mu_message_t msg)
{
  mu_stream_t progstream, msgstream;
  int status, rc;
  
  status = mu_command_stream_create (&progstream, cmd, MU_STREAM_WRITE);
  if (status)
    {
      mu_error (_("Cannot pipe to %s: %s"), cmd, mu_strerror (status));
      return status;
    }

  mu_message_get_streamref (msg, &msgstream);
  status = mu_stream_copy (progstream, msgstream, 0, NULL);
  rc = mu_stream_close (progstream);

  if (status == 0 && rc)
    status = rc;

  mu_stream_destroy (&progstream);
  mu_stream_destroy (&msgstream);
  
  if (status)
    {
      mu_error (_("Sending data to %s failed: %s"), cmd,
		  mu_strerror (status));
    }
  return status;
}