/*
   This file is part of Mailfromd.
   Copyright (C) 2003-2022 Sergey Poznyakoff.

   Mailfromd 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.

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

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <stdlib.h>
#include <unistd.h>
#ifdef HAVE_GETOPT_H
# include <getopt.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <stdarg.h>
#include <ctype.h>
#include <fcntl.h>
#include <sys/types.h>
#include <dirent.h>
#include <sysexits.h>
#ifdef HAVE_READLINE_READLINE_H
# include <readline/readline.h>
# include <readline/history.h>
#endif

#include <pwd.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <netinet/in.h>

#include <mailutils/mailutils.h>
#include <gacopyz.h>

#include "libmf.h"

#if defined(USE_GNUTLS) && defined(HAVE_GNUTLS_GNUTLS_H)
# include <gnutls/gnutls.h>
# define HAVE_TLS
#endif /* USE_GNUTLS and HAVE_GNUTLS_GNUTLS_H */

FILE *trace = NULL;		/* diagnostic output */
int port = 0;			/* Port number (for smtp mode) */

int verbose;

char *user; /* When started as root, switch to this user privileges */
struct mf_gid_list *grouplist; /* List of additional groups to be retained */

char *sender_hostname;
struct sockaddr *sender_sockaddr;

int mta_daemon (void);
int mta_stdio (void);

int (*mta_mode) (void) = mta_stdio;
char *trace_name = NULL;
int append;
int statedir_option;

#ifdef HAVE_TLS
char *tls_cert;			/* TLS sertificate */
char *tls_key;			/* TLS key */
char *tls_cafile;

#define DH_BITS 768
#define enable_tls() \
 (tls_cafile != NULL || (tls_cert != NULL && tls_key != NULL))
void tls_init (void);

gnutls_dh_params dh_params;
static gnutls_certificate_server_credentials x509_cred;
#endif /* HAVE_TLS */

int interactive;
char *prompt = "(mtasim) ";

void error (const char *, ...);
void smtp_reply (int, char *, ...);
void reset_capa (char *);
void shell_help (void);

void shell (int argc, char **argv);
int define_macro (char *arg);

#define R_CONT     0x8000
#define R_CODEMASK 0xfff

/* Milter-related options */
char *milter_port;
size_t max_body_chunk = 65535;
unsigned long milter_version_option = 0;
unsigned long milter_proto_option = 0;
unsigned long milter_acts_option = 0;
int gacopyz_log_mask;
struct timeval milter_timeouts[GACOPYZ_TO_COUNT] = {
  { GACOPYZ_WRITE_TIMEOUT, 0 },
  { GACOPYZ_READ_TIMEOUT, 0 },
  { GACOPYZ_EOM_TIMEOUT, 0 },
  { GACOPYZ_CONNECT_TIMEOUT, 0 }
};


static void
priv_setup ()
{
  if (getuid() == 0 && user)
    {
      struct passwd *pw = getpwnam (user);
      if (!pw)
	{
	  mu_error(_("no such user: %s"), user);
	  exit (EX_SOFTWARE);
	}
      if (pw && switch_to_privs (pw->pw_uid, pw->pw_gid, grouplist))
	exit (EX_SOFTWARE);
    }
}


gacopyz_srv_t gsrv;

void
update_nrcpts (char *name, unsigned n)
{
  char buf[128];
  snprintf (buf, sizeof buf, "%u", n);
  gacopyz_srv_define_macro (gsrv, name, buf);
}


static mu_list_t defnlist;

int
defer_define_macro (char const *arg)
{
  char *copy, *p;

  copy = mu_strdup (arg);
  p = strchr (copy, '=');
  if (!p)
    {
      free (copy);
      return 1;
    }
  *p = 0;
  if (!defnlist)
    {
      mu_list_create (&defnlist);
      mu_list_set_destroy_item (defnlist, mu_list_free_item);
    }
  mu_list_append (defnlist, copy);
  return 0;
}

static int
do_define (void *item, void *data)
{
  char *name = item;
  char *value = name + strlen (name) + 1;
  gacopyz_srv_t srv = data;
  gacopyz_srv_define_macro (srv, name, value);
  return 0;
}

void
flush_deferred_defns (gacopyz_srv_t srv)
{
  if (srv)
    mu_list_foreach (defnlist, do_define, srv);
  mu_list_destroy (&defnlist);
}


/* Expect stuff */
static char expected_code[4];

void
free_expected_code ()
{
  expected_code[0] = 0;
}

int
set_expected_code (const char *str)
{
  size_t len = strlen (str);
  if (len == 0 || !strchr ("12345", str[0]))
    return 1;

  if (len > 1)
    {
      if (!mu_isdigit (str[1])
	  || (len > 2 && !mu_isdigit (str[2])))
	return 1;
    }

  if (len > 3)
    len = 3;
  memcpy (expected_code, str, len);
  expected_code[len] = 0;
  return 0;
}

void
check_expected_code (char *str)
{
  int i;
  
  for (i = 0; i < 3 && expected_code[i]; i++)
    if (str[i] != expected_code[i])
      {
	/* fill in the expected code */
	for (i = strlen(expected_code); i < 3; i++)
	  expected_code[i] = '.';
	mu_error (_("expected %s, but got %3.3s"), expected_code, str);
	
	if (gsrv)
	  {
	    gacopyz_srv_abort (gsrv);
	    gacopyz_srv_quit (gsrv);
	    gacopyz_srv_close (gsrv);
	    gacopyz_srv_destroy (&gsrv);
	  }
	exit (1);
      }
  expected_code[0] = 0;
}


static char prog_doc[] = N_("mtasim -- MTA simulator for mailfromd");

#ifndef WITH_READLINE
# define OPT_INTERACTIVE MU_OPTION_HIDDEN
#else
# define OPT_INTERACTIVE MU_OPTION_DEFAULT
#endif

static void
opt_mta_mode(struct mu_parseopt *po, struct mu_option *opt, char const *arg)
{
  switch (arg[0])
    {
    case 'd':
      mta_mode = mta_daemon;
      break;
      
    case 's':
      mta_mode = mta_stdio;
      break;
      
    default:
      mu_parseopt_error (po, _("unsupported mode"));
      exit(po->po_exit_error);
    }
}

static void
opt_group(struct mu_parseopt *po, struct mu_option *opt, char const *arg)
{
  struct group *group = getgrnam(arg);
  if (group)
    {
      if (!grouplist)
	grouplist = mf_gid_list_alloc ();
      mf_gid_list_add (grouplist, group->gr_gid);
    }
  else
    {
      mu_parseopt_error(po, _("unknown group: %s"), arg);
      exit (EX_DATAERR);
    }
}

static void
opt_define (struct mu_parseopt *po, struct mu_option *opt, char const *arg)
{
  if (defer_define_macro (arg)) 
    {
      mu_parseopt_error (po, _("wrong assignment format: %s"), arg);
      exit (po->po_exit_error);
    }
}

static void
opt_milter_version (struct mu_parseopt *po, struct mu_option *opt,
		    char const *arg)
{
  char *p;
  unsigned long maj, min, pat;

  errno = 0;
  maj = strtoul (arg, &p, 0);
  if (errno != 0 || p == arg)
    /* error */;
  else if (*p == 0)
    {
      milter_version_option = maj;
      return;
    }
  else if (*p == '.')
    {
      arg = p + 1;
      min = strtoul (arg, &p, 0);
      if (errno != 0 || p == arg)
	/* error */;
      else if (*p == '.')
	{
	  arg = p + 1;
	  pat = strtoul (arg, &p, 0);
	  if (errno != 0 || p == arg)
	    /* error */;
	  else if (*p == 0)
	    {
	      milter_version_option = GACOPYZ_SM_MKVER (maj, min, pat);
	      return;
	    }
	}
      else if (*p == 0)
	{
	  milter_version_option = GACOPYZ_SM_MKVER (maj, min, 0);
	  return;
	}
    }
  mu_parseopt_error (po, _("invalid version syntax (near %s)"), p);
  exit (po->po_exit_error);
}

static int
strtotv (char const *arg, char **endp, struct timeval *rettv)
{
  char *p;
  struct timeval tv;

  errno = 0;
  tv.tv_sec = strtoul (arg, &p, 10);
  if (errno)
    goto err;
  else if (p == arg)
    {
      errno = EINVAL;
      goto err;
    }
  if (*p == '.')
    {
      errno = 0;
      tv.tv_usec = strtoul (p + 1, &p, 10);
      if (errno)
	goto err;
    }
  else
    tv.tv_usec = 0;

  *rettv = tv;
 err:
  if (endp)
    *endp = p;
  return errno ? -1 : 0;
}

static void
opt_milter_timeout (struct mu_parseopt *po, struct mu_option *opt,
		    char const *arg)
{
  while (*arg)
    {
      int ind;
      struct timeval tv;
      char *p;
      
      while (*arg && isspace (*arg))
	arg++;
      switch (*arg)
	{
	case 'w':
	case 'W':
	case 's':
	case 'S':
	  ind = GACOPYZ_TO_WRITE;
	  break;
	  
	case 'r':
	case 'R':
	  ind = GACOPYZ_TO_READ;
	  break;

	case 'e':
	case 'E':
	  ind = GACOPYZ_TO_EOM;
	  break;

	case 'c':
	case 'C':
	  ind = GACOPYZ_TO_CONNECT;
	  break;
	  
	default:
	  mu_parseopt_error (po, _("unknown timeout: %c"), *arg);
	  exit (po->po_exit_error);
	}
      if (*++arg != '=')
	{
	  mu_parseopt_error (po, _("missing '=' after %c"), *arg);
	  exit (po->po_exit_error);
	}

      arg++;
      if (strtotv (arg, &p, &tv) == 0)
	{
	  milter_timeouts[ind] = tv;
	  while (*p && isspace (*p))
	    p++;
	  if (*p == ',')
	    arg = p + 1;
	  else if (*p == 0)
	    break;
	  else
	    {
	      mu_parseopt_error (po, _("unexpected character at %s"), p);
	      exit (po->po_exit_error);
	    }
	}
      else
	{
	  if (errno == EINVAL)
	    mu_parseopt_error (po, _("invalid timeout near %s"), p);
	  else
	    mu_parseopt_error (po, _("value out of range: %s"), arg);
	  exit (po->po_exit_error);
	}
    }
}

static void
_sockaddr_error_getopt (void *data, const char *msg, const char *arg)
{
  struct mu_parseopt *po = data;
  
  if (arg)
    mu_parseopt_error (po, "%s: %s", msg, arg);
  else
    mu_parseopt_error (po, "%s", msg);
  exit (po->po_exit_error);
}

/* family [hostname address [port]] */
static int
convert_sender_sockaddr (int argc, char **argv,
			 void (*errfun)(void *, const char *, const char *),
			 void *errdata)
{
  int family;
  char *p;
  struct sockaddr *sa;
  
  if (argc < 1 || argc > 4)
    {
      errfun (errdata, "Bad number of arguments", NULL);
      return -1;
    }

  if (strcmp (argv[0], "stdio") == 0)
    {
      switch (argc)
	{
	case 1:
	  sender_hostname = mu_strdup ("localhost");
	  break;

	case 2:
	  sender_hostname = mu_strdup (argv[1]);
	  break;

	default:
	  errfun (errdata, "Too many arguments for this address family", NULL);
	  return -1;
	}
      return 0;
    }
  
  if (argc < 3)
    {
      errfun (errdata, "Too few arguments", NULL);
      return -1;
    }

  if (strcmp (argv[0], "unix") == 0)
    family = AF_UNIX;
  else if (strcmp (argv[0], "inet") == 0)
    family = AF_INET;
  else if (strcmp (argv[0], "inet6") == 0)
    family = AF_INET6;
  else
    {
      family = strtoul (argv[0], &p, 10);
      if (*p)
	{
	  errfun (errdata, "Unrecognized family", NULL);
	  return -1;
	}
    }

  switch (family)
    {
    case AF_UNIX: {
      struct sockaddr_un *sun;

      if (argc == 4)
	{
	  errfun (errdata, "Too many arguments for this address family", NULL);
	  return -1;
	}
      sun = mu_alloc (sizeof (sun) + strlen (argv[2]) + 1);
      sun->sun_family = family;
      strcpy (sun->sun_path, argv[2]);
      sa = (struct sockaddr*) sun;
      break;
    }
      
    case AF_INET: {
      struct sockaddr_in *s_in;
      unsigned long n;
      
      if (argc < 4)
	{
	  errfun (errdata, "Not enough arguments for this address family",
		  NULL);
	  return -1;
	}
      s_in = mu_alloc (sizeof *s_in);
      s_in->sin_family = family;
      if (inet_aton (argv[2],
		     (struct in_addr *) &s_in->sin_addr) != 1)
	{
	  errfun (errdata, "Invalid IP address", NULL);
	  free (s_in);
	  return -1;
	}
      
      errno = 0;
      n = strtoul (argv[3], &p, 10);
      if (*p || errno || (s_in->sin_port = n) != n)
	{
	  errfun (errdata, "Invalid port number", NULL);
	  return -1;
	}
      s_in->sin_port = htons (s_in->sin_port);
      sa = (struct sockaddr*) s_in;
      break;
    }
      
    case AF_INET6: {
#ifdef GACOPYZ_IPV6
      struct addrinfo *res;
      int rc;
      
      if (argc < 4)
	{
	  errfun (errdata, "Not enough arguments for this address family",
		  NULL);
	  return -1;
	}
      rc = getaddrinfo(argv[2], argv[3], NULL, &res);
      if (rc)
	{
	  errfun (errdata, "Invalid address or service",
		  gai_strerror(rc));
	  return -1;
	}

      sa = mu_alloc (res->ai_addrlen);
      memcpy (sa, res->ai_addr, res->ai_addrlen);
      freeaddrinfo (res);
#endif
      break;
    }
      
    default:
      errfun (errdata, "Unsupported family", NULL);
      return -1;
    }

  free (sender_hostname);
  free (sender_sockaddr);
  sender_hostname = mu_strdup (argv[1]);
  sender_sockaddr = sa;
  return 0;
}

static void
opt_sender_sockaddr (struct mu_parseopt *po, struct mu_option *opt,
		     char const *arg) 
{
  struct mu_wordsplit ws;
  int rc;
	
  ws.ws_delim = ",";
  rc = mu_wordsplit (arg, &ws, MU_WRDSF_DEFFLAGS|MU_WRDSF_DELIM);
  if (rc)
    {
      mu_parseopt_error (po, _("error parsing --sender-sockaddr argument: %s"),
			 mu_wordsplit_strerror (&ws));
      exit (po->po_exit_error);
    }
  convert_sender_sockaddr (ws.ws_wordc, ws.ws_wordv,
			   _sockaddr_error_getopt, po);
  mu_wordsplit_free (&ws);
}

static void
opt_gacopyz_log (struct mu_parseopt *po, struct mu_option *opt,
		 char const *arg)
{
  int lev = gacopyz_string_to_log_level (arg);
  if (lev == -1)
    {
      mu_parseopt_error (po, _("%s: invalid log level"), arg);
      exit (po->po_exit_error);
    }
  gacopyz_log_mask = SMI_LOG_FROM (lev);
}

static struct mu_option mtasim_options[] = {
  MU_OPTION_GROUP (N_("Mode selection")),
  { NULL, 'b', N_("MODE"), MU_OPTION_DEFAULT,
    N_("set MTA mode (-bd or -bs)"),
    mu_c_string, NULL, opt_mta_mode },
  { "stdio", 0, NULL, MU_OPTION_DEFAULT,
    N_("use the SMTP protocol on standard input and output (same as -bs)"),
    mu_c_string, NULL, opt_mta_mode, "s" },
  { "daemon", 0, NULL, MU_OPTION_DEFAULT,
    N_("run as daemon (same as -bd)"),
    mu_c_string, NULL, opt_mta_mode, "d" },
  { "user", 'u', N_("NAME"), MU_OPTION_DEFAULT,
    N_("run with this user privileges"),
    mu_c_string, &user },
  { "group", 'g', N_("NAME"), MU_OPTION_DEFAULT,
    N_("retain the supplementary group NAME when switching to user "
       "privileges"),
    mu_c_string, NULL, opt_group },

  MU_OPTION_GROUP (N_("Operation modifiers")),
  { "define", 'D', N_("MACRO=VALUE"), MU_OPTION_DEFAULT,
    N_("define Sendmail macro"),
    mu_c_string, NULL, opt_define },
  { "port", 'X', N_("PORT"), MU_OPTION_DEFAULT,
    N_("communicate with Milter PORT"),
    mu_c_string, &milter_port },
  { "statedir", 0, NULL, MU_OPTION_DEFAULT,
    N_("pass temporary directory to mailfromd as its state dir (with -Xauto)"),
    mu_c_bool, &statedir_option },
  { "body-chunk", 0, N_("NUMBER"), MU_OPTION_DEFAULT,
    N_("set the body chunk for xxfi_body calls"),
    mu_c_size, &max_body_chunk },
  { "milter-version", 0, N_("VER"), MU_OPTION_DEFAULT,
    N_("force using the given Milter protocol version number"),
    mu_c_string, NULL, opt_milter_version },
  { "milter-actions", 0, N_("BITMASK"), MU_OPTION_DEFAULT,
    N_("force the given Milter actions"),
    mu_c_ulong, &milter_acts_option },
  { "milter-proto", 0, N_("BITMASK"), MU_OPTION_DEFAULT,
    N_("set Milter protocol capabilities"),
    mu_c_ulong, &milter_proto_option },
  { "milter-timeout", 0, N_("F=V[,F=V...]"), MU_OPTION_DEFAULT,
    N_("set milter timeouts"),
    mu_c_string, NULL, opt_milter_timeout },
  { "sender-sockaddr", 0,
    N_("FAMILY[,HOSTNAME,ADDRESS[,PORT]]"), MU_OPTION_DEFAULT,
    N_("set sender socket address"),
    mu_c_string, NULL, opt_sender_sockaddr },

  { "interactive", 0, NULL, OPT_INTERACTIVE,
    N_("interactive mode"),
    mu_c_bool, &interactive },
  
  { "prompt", 0, N_("STRING"), MU_OPTION_DEFAULT,
    N_("set readline prompt"),
    mu_c_string, &prompt },

  MU_OPTION_GROUP(N_("Debugging and tracing")),
  { "append", 'a', NULL, MU_OPTION_DEFAULT,
    N_("append to the trace file"),
    mu_c_bool, &append },
  { "trace-file", 0, N_("FILE"), MU_OPTION_DEFAULT,
    N_("set name of the trace file"),
    mu_c_string, &trace_name },
  { "verbose", 'v', NULL, MU_OPTION_DEFAULT,
    N_("increase verbosity level"),
    mu_c_incr, &verbose },
  { "gacopyz-log", 0, N_("LEVEL"), MU_OPTION_DEFAULT,
    N_("set Gacopyz log level"),
    mu_c_string, NULL, opt_gacopyz_log },

#ifdef HAVE_TLS
  MU_OPTION_GROUP (N_("TLS options")),
  { "tls-cert", 0, N_("FILE"), MU_OPTION_DEFAULT,
    N_("set name of the TLS certificate file"),
    mu_c_string, &tls_cert },
  { "tls-ca", 0, N_("FILE"), MU_OPTION_DEFAULT,
    N_("set name of the TLS CA file"),
    mu_c_string, &tls_cafile },
  { "tls-key", 0, N_("FILE"), MU_OPTION_DEFAULT,
    N_("set name of the TLS key file"),
    mu_c_string, &tls_key},
#endif
  
  MU_OPTION_END
}, *options[] = { mtasim_options, NULL };

void
alloc_die_func ()
{
  mu_error (_("not enough memory"));
  abort ();
}

static int
portspec_p (const char *str)
{
  size_t len = strlen (str);
  static char *proto[] = { "/", "unix:", "local:", "inet:",  "inet6:", NULL };
  char **p;

  for (p = proto; *p; p++)
    {
      size_t n = strlen (*p);
      if (len > n && memcmp (str, *p, n) == 0)
	return 1;
    }
  return 0;
}


pid_t child_pid;
static char *tmpdir;

static int got_sigchld;

static RETSIGTYPE
sig_child (int sig)
{
  int status;
  
  got_sigchld = 1;
  waitpid((pid_t)-1, &status, 0);
}

static int rmdir_r (const char *name);

static int
recursive_rmdir (const char *name)
{
  int rc;
  DIR *dir;
  struct dirent *ent;

  if (chdir (name))
    {
      mu_error (_("cannot change to directory %s: %s"),
		name, mu_strerror (errno));
      return 1;
    }
  
  dir = opendir (".");
  if (!dir)
    {
      mu_error (_("cannot open directory %s: %s"), name, mu_strerror (errno));
      return 1;
    }

  for (rc = 0; rc == 0 && (ent = readdir (dir));)
    {
      struct stat st;
      
      if (strcmp (ent->d_name, ".") == 0
	  || strcmp (ent->d_name, "..") == 0)
	continue;

      if (stat (ent->d_name, &st) && errno != ENOENT)
	{
	  mu_error (_("cannot stat file `%s': %s"),
		    name, mu_strerror (errno));
	  rc = 1;
	}
      else if (S_ISDIR (st.st_mode))
	rc = rmdir_r (ent->d_name);
      else if ((rc = unlink (ent->d_name)) != 0 && errno != ENOENT)
	mu_error (_("cannot unlink %s: %s"), ent->d_name, mu_strerror (errno));
    }
  closedir (dir);
  return rc;
}

struct cwd
{
  int cwd_fd;
  char *cwd_name;
};

static int
save_cwd (struct cwd *p)
{
#ifndef O_SEARCH
# define O_SEARCH 0
#endif
  p->cwd_fd = open (".", O_SEARCH);
  if (p->cwd_fd != -1)
    {
      int flags = fcntl (p->cwd_fd, F_GETFD, 0);
      if (flags != -1 && fcntl (p->cwd_fd, F_SETFD, flags | FD_CLOEXEC) != -1)
	{
	  p->cwd_name = NULL;
	  return 0;
	}
      close (p->cwd_fd);
      p->cwd_fd = -1;
    }

  p->cwd_name = mu_getcwd ();
  if (!p->cwd_name)
    return -1;
  
  return 0;
}

static int
restore_cwd (struct cwd *p)
{
  int rc;
  
  if (p->cwd_fd != -1)
    rc = fchdir (p->cwd_fd);
  else if (p->cwd_name)
    {
      rc = chdir (p->cwd_name);
      free (p->cwd_name);
    }
  else
    rc = 0;
  return rc;
}

static int
rmdir_r (const char *name)
{
  int rc;
  struct cwd cwd;

  if (save_cwd (&cwd))
    {
      mu_error (_("cannot save current directory: %s"), mu_strerror (errno));
      return 1;
    }
  rc = recursive_rmdir (name);
  if (restore_cwd (&cwd))
    {
      mu_error (_("cannot restore current directory: %s"),
		mu_strerror (errno));
      rc = 1;
    }

  if (rc == 0 && rmdir (name))
    {
      mu_error (_("cannot remove directory %s: %s"),
		name, mu_strerror (errno));
      return 1;
    }

  return rc;
}

void
stop_mailfromd (void)
{
  if (child_pid > 0)
    {
      int status;
      pid_t pid;
      
      signal (SIGCHLD, SIG_DFL);
      kill (child_pid, SIGTERM);
      pid = waitpid (child_pid, &status, 0);
      if (pid == (pid_t) -1)
	mu_error ("waitpid: %s", mu_strerror (errno));
      else if (WIFEXITED (status))
	{
	  status = WEXITSTATUS (status);
	  if (status != 0)
	    mu_error (_("mailfromd exited with status %d"), status);
	}
      else if (WIFSIGNALED (status)) 
	mu_error (_("mailfromd terminated on signal %d"), WTERMSIG (status));
      else
	mu_error (_("mailfromd terminated with unrecognized status"));
    }
  
  rmdir_r (tmpdir);
}

void
start_mailfromd (int argc, char **argv)
{
  tmpdir = mu_strdup ("/tmp/mtasim-XXXXXX");
  
  if (!mkdtemp (tmpdir))
    {
      mu_error (_("cannot create temporary directory (%s): %s"),
		tmpdir, mu_strerror (errno));
      exit (EX_OSERR);
    }
  atexit (stop_mailfromd);

  mu_asprintf (&milter_port, "unix://%s/milter", tmpdir);

  signal (SIGCHLD, sig_child);
  
  child_pid = fork ();

  if (child_pid == -1)
    {
      mu_error (_("cannot fork: %s"), mu_strerror (errno));
      exit (EX_OSERR);
    }

  if (child_pid == 0)
    {
      int xargc = argc + 7 + (statedir_option ? 2 : 0);
      char **xargv = mu_alloc ((xargc + 1) * sizeof xargv[0]);
      int i;
      char *callout_port = NULL;
      
      mu_asprintf (&callout_port, "unix://%s/callout", tmpdir);

      xargv[0] = "mailfromd";
      for (i = 1; i <= argc; i++)
	xargv[i] = argv[i-1];
      xargv[i++] = "--mtasim";
      xargv[i++] = "--port";
      xargv[i++] = milter_port;
      xargv[i++] = "--callout-socket";
      xargv[i++] = callout_port;
      if (statedir_option)
	{
	  xargv[i++] = "--state-directory";
	  xargv[i++] = tmpdir;
	}
      xargv[i] = 0;

      if (verbose)
	{
	  fprintf (stderr, "executing ");
	  for (i = 0; i < xargc; i++)
	    fprintf (stderr, "%s ", xargv[i]);
	  fprintf (stderr, "\n");
	}
      
      execvp (xargv[0], xargv);

      mu_error (_("cannot execute mailfromd: %s"), mu_strerror (errno));
      exit (127);
    }

  if (verbose)
    fprintf (stderr, "mailfromd PID: %lu\n", (unsigned long) child_pid);

  while (access (milter_port + 5, F_OK))
    {
      struct timeval tv;
      
      if (got_sigchld)
	{
	  mu_error (_("child process exited unexpectedly"));
	  exit (EX_UNAVAILABLE);
	}

      tv.tv_sec = 0;
      tv.tv_usec = 5;
      select (0, NULL, NULL, NULL, &tv);
    }
}


#ifdef WITH_READLINE
static char **mta_command_completion (char *cmd, int start, int end);
static char *get_history_file_name (void);
#endif

struct mu_cli_setup cli = {
	.optv = options,
	.prog_doc = prog_doc,
};

int
main (int argc, char **argv)
{
  int status;

  mf_init_nls ();
  
  interactive = isatty(0);
  mu_alloc_die_hook = alloc_die_func;
  
  mf_getopt (&cli, &argc, &argv, NULL, MF_GETOPT_NO_CONFIG);

  priv_setup ();
  
#ifdef WITH_READLINE
  if (interactive)
    {
      rl_readline_name = mu_program_name;
      rl_attempted_completion_function = (rl_completion_func_t*) mta_command_completion;
      read_history (get_history_file_name ());
    }
#endif

  if (trace_name)
    {
      char *mode = append ? "a" : "w";
      trace = fopen (trace_name, mode);
      if (!trace)
	{
	  mu_error (_("cannot open trace output: %s"), trace_name);
	  return 1;
	}
    }

  if (!mta_mode)
    {
      mu_error (_("use either --stdio or --daemon"));
      exit (EX_USAGE);
    }

  if (milter_port)
    {
      int rc;

      if (gacopyz_log_mask == 0)
	{
	  gacopyz_log_mask = SMI_DEFAULT_LOG_MASK;
	  if (verbose)
	    gacopyz_log_mask |= SMI_LOG_MASK (SMI_LOG_DEBUG);
	}
      
      gacopyz_set_logger (gacopyz_stderr_log_printer);
      
      if (strcmp (milter_port, "auto") == 0)
	start_mailfromd (argc, argv);

      if (portspec_p (milter_port))
	rc = gacopyz_srv_create (&gsrv, "Test", milter_port, gacopyz_log_mask);
      else
	rc = gacopyz_srv_create_X (&gsrv, milter_port, gacopyz_log_mask);

      if (rc != MI_SUCCESS)
	{
	  mu_error (_("cannot create gacopyz server"));
	  exit (EX_UNAVAILABLE);
	}

      if (milter_version_option)
	{
	  unsigned long acts = SMFI_DEFAULT_ACTS, proto = SMFI_DEFAULT_PROT;

	  if (milter_version_option == 2 || milter_version_option == 3)
	    {
	      acts = SMFI_V2_ACTS;
	      proto = SMFI_V2_PROT;
	    }
	  
	  gacopyz_srv_set_version (gsrv, milter_version_option);
	  gacopyz_srv_set_protocol (gsrv, proto);
	  gacopyz_srv_set_actions (gsrv, acts);
	}

      if (milter_proto_option)
	gacopyz_srv_set_protocol (gsrv, milter_proto_option);
      if (milter_acts_option)
	gacopyz_srv_set_actions (gsrv, milter_acts_option);
      
      gacopyz_srv_set_all_timeouts (gsrv, milter_timeouts);
      
      if (gacopyz_srv_open (gsrv) != MI_SUCCESS)
	{
	  mu_error (_("cannot connect to the milter using %s"), milter_port);
	  exit (EX_UNAVAILABLE);
	}
      gacopyz_srv_negotiate (gsrv);
      if (sender_hostname)
	gacopyz_srv_connect (gsrv, sender_hostname, sender_sockaddr);
    }
  
  flush_deferred_defns (gsrv);

#ifdef HAVE_TLS
  tls_init ();
#endif
  status = mta_mode ();

  if (trace)
    fclose (trace);

#ifdef WITH_READLINE
  if (interactive)
    write_history (get_history_file_name ());
#endif
  
  return status;
}

static void *in, *out;

static const char *
_def_strerror (int rc)
{
  return rc == -1 ? _("end of file reached") : strerror (rc);
}

static int
_def_write (void *sd, char *data, size_t size, size_t * nbytes)
{
  int n = write (*(int*) sd, data, size);
  if (n != size)
    return errno;
  if (nbytes)
    *nbytes = n;
  return 0;
}

static int
_def_read (void *sd, char *data, size_t size, size_t * nbytes)
{
  int n = read (*(int*) sd, data, size);
  if (n && n != size)
    return errno;
  if (nbytes)
    *nbytes = n;
  return 0;
}

static int
_def_close (void *sd)
{
  return close (*(int*) sd);
}

int (*_mta_read) (void *, char *, size_t, size_t *) = _def_read;
int (*_mta_write) (void *, char *, size_t, size_t *) = _def_write;
int (*_mta_close) (void *) = _def_close;
const char *(*_mta_strerror) (int) = _def_strerror;

#ifdef HAVE_TLS

static void
_tls_cleanup_x509 (void)
{
  if (x509_cred)
    gnutls_certificate_free_credentials (x509_cred);
}

static void
generate_dh_params (void)
{
  gnutls_dh_params_init (&dh_params);
  gnutls_dh_params_generate2 (dh_params, DH_BITS);
}

void
tls_init (void)
{
  if (!enable_tls ())
    return;

  gnutls_global_init ();
  atexit (gnutls_global_deinit);
  gnutls_certificate_allocate_credentials (&x509_cred);
  atexit (_tls_cleanup_x509);
  if (tls_cafile)
    {
      int rc = gnutls_certificate_set_x509_trust_file (x509_cred,
						       tls_cafile,
						       GNUTLS_X509_FMT_PEM);
      if (rc < 0)
	{
	  gnutls_perror (rc);
	  return;
	}
    }
  if (tls_cert && tls_key)
    gnutls_certificate_set_x509_key_file (x509_cred,
					  tls_cert, tls_key,
					  GNUTLS_X509_FMT_PEM);

  generate_dh_params ();
  gnutls_certificate_set_dh_params (x509_cred, dh_params);
}

static ssize_t
_tls_fd_pull (gnutls_transport_ptr fd, void *buf, size_t size)
{
  int rc;
  do
    {
      rc = read ((int) fd, buf, size);
    }
  while (rc == -1 && errno == EAGAIN);
  return rc;
}

static ssize_t
_tls_fd_push (gnutls_transport_ptr fd, const void *buf, size_t size)
{
  int rc;
  do
    {
      rc = write ((int) fd, buf, size);
    }
  while (rc == -1 && errno == EAGAIN);
  return rc;
}

static const char *
_tls_strerror (int rc)
{
  return gnutls_strerror (rc);
}

static int
_tls_write (void *sd, char *data, size_t size, size_t * nbytes)
{
  int rc;

  do
    rc = gnutls_record_send (sd, data, size);
  while (rc == GNUTLS_E_INTERRUPTED || rc == GNUTLS_E_AGAIN);
  if (rc >= 0)
    {
      if (nbytes)
	*nbytes = rc;
      return 0;
    }
  return rc;
}

static int
_tls_read (void *sd, char *data, size_t size, size_t * nbytes)
{
  int rc = gnutls_record_recv (sd, data, size);
  if (rc >= 0)
    {
      if (nbytes)
	*nbytes = rc;
      return 0;
    }
  return rc;
}

static int
_tls_close (void *sd)
{
  if (sd)
    {
      gnutls_bye (sd, GNUTLS_SHUT_RDWR);
      gnutls_deinit (sd);
    }
  return 0;
}

static gnutls_session
tls_session_init (void)
{
  gnutls_session session = 0;
  int rc;

  gnutls_init (&session, GNUTLS_SERVER);
  gnutls_set_default_priority (session);
  gnutls_credentials_set (session, GNUTLS_CRD_CERTIFICATE, x509_cred);
  gnutls_certificate_server_set_request (session, GNUTLS_CERT_REQUEST);
  gnutls_dh_set_prime_bits (session, DH_BITS);

  gnutls_transport_set_pull_function (session, _tls_fd_pull);
  gnutls_transport_set_push_function (session, _tls_fd_push);

  gnutls_transport_set_ptr2 (session,
			     (gnutls_transport_ptr) in,
			     (gnutls_transport_ptr) out);
  rc = gnutls_handshake (session);
  if (rc < 0)
    {
      gnutls_deinit (session);
      gnutls_perror (rc);
      return 0;
    }

  return (gnutls_session) session;
}

void
smtp_starttls (void)
{
  gnutls_session session;

  smtp_reply (220, "Ready to start TLS");

  session = tls_session_init ();
  if (session)
    {
      in = out = session;
      _mta_read = _tls_read;
      _mta_write = _tls_write;
      _mta_close = _tls_close;
      _mta_strerror = _tls_strerror;
      reset_capa ("STARTTLS");
    }
  else
    smtp_reply (530, "TLS negotiation failed");
}

#endif /* HAVE_TLS */

void
smtp_send_string (char *str, size_t len)
{
  int rc;
  if (!len)
    len = strlen (str);
  if (trace)
    fprintf (trace, "%s\n", str);
  rc = _mta_write (out, str, len, NULL);
  if (rc == 0 && str[len-1] != '\n')
    rc = _mta_write (out, "\r\n", 2, NULL);
  if (rc)
    {
      mu_error (_("write failed: %s"), _mta_strerror (rc));
      abort ();
    }
  check_expected_code (str);
}

void
smtp_reply (int code, char *fmt, ...)
{
  va_list ap;
  int cont = code & R_CONT ? '-' : ' ';
  static char obuf[512];
  int n, rc;

  va_start (ap, fmt);
  n = snprintf (obuf, sizeof obuf, "%d%c", code & R_CODEMASK, cont);
  n += vsnprintf (obuf + n, sizeof obuf - n, fmt, ap);
  va_end (ap);
  if (trace)
    fprintf (trace, "%s\n", obuf);
  n += snprintf (obuf + n, sizeof obuf - n, "\r\n");
  rc = _mta_write (out, obuf, n, NULL);
  if (rc)
    {
      mu_error (_("write failed: %s"), _mta_strerror (rc));
      abort ();
    }
  check_expected_code (obuf);
}

int
get_input_line (char *buf, size_t bufsize)
{
  int i, rc;

#ifdef WITH_READLINE
  if (interactive)
    {
      char *p = readline (prompt);
      if (!p)
	return -1;
      strncpy (buf, p, bufsize);
      buf[bufsize - 1] = 0;
      add_history (buf);
      strcat (buf, "\n");
      free (p);
      return strlen (buf);
    }
#endif
  
  for (i = 0; i < bufsize - 1; i++)
    {
      size_t n;
      rc = _mta_read (in, buf + i, 1, &n);
      if (rc)
	{
	  mu_error (_("read failed: %s"), _mta_strerror (rc));
	  exit (EX_IOERR);
	}
      if (n == 0)
	break;
      if (buf[i] == '\n')
	{
	  if (buf[i-1] == '\r')
	    buf[i-1] = '\n';
          else
            i++;
	  break;
	}
    }
  buf[i] = 0;
  return i;
}
 
#define STATE_INIT    0
#define STATE_EHLO    1
#define STATE_MAIL    2
#define STATE_RCPT    3
#define STATE_HEADERS 4
#define STATE_DATA    5
#define STATE_QUIT    6
#define STATE_DOT     7

#define KW_EHLO      0
#define KW_HELO      1
#define KW_MAIL      2
#define KW_RCPT      3
#define KW_DATA      4
#define KW_HELP      5
#define KW_QUIT      6
#define KW_STARTTLS  7
#define KW_RSET      8

struct keyword
{
  char *name;
  int code;
};

static struct keyword kw[] =
  {
    { "EHLO", KW_EHLO },
    { "HELO", KW_HELO },
    { "MAIL", KW_MAIL },
    { "RCPT", KW_RCPT },
    { "DATA", KW_DATA },
    { "HELP", KW_HELP },
    { "QUIT", KW_QUIT },
    { "HELP", KW_HELP },
#ifdef HAVE_TLS
    { "STARTTLS", KW_STARTTLS },
#endif
    { "RSET", KW_RSET },
    { NULL },
  };

int
smtp_kw (const char *name)
{
  int i;

  for (i = 0; kw[i].name != NULL; i++)
    if (strcasecmp (name, kw[i].name) == 0)
      return kw[i].code;
  return -1;
}

#ifdef WITH_READLINE

#define HISTFILE_SUFFIX "_history"

static char *
get_history_file_name ()
{
  static char *filename = NULL;

  if (!filename)
    {
      size_t size;
      char *home = getenv ("HOME");
      if (!home)
	{
	  struct passwd *pw = getpwuid (getuid ());
	  if (!pw)
	    return NULL;
	  home = pw->pw_dir;
	}
      size = strlen (home) + 2 + strlen (rl_readline_name)
      	     + sizeof HISTFILE_SUFFIX;
      filename = mu_alloc (size);
      strcpy (filename, home);
      strcat (filename, "/.");
      strcat (filename, rl_readline_name);
      strcat (filename, HISTFILE_SUFFIX);
  }
  return filename;
}

static char *
mta_command_generator (const char *text, int state)
{
  static int i, len;
  const char *name;
  
  if (!state)
    {
      i = 0;
      len = strlen (text);
    }

  while ((name = kw[i].name))
    {
      i++;
      if (strncasecmp (name, text, len) == 0)
	return mu_strdup (name);
    }

  return NULL;
}

char **
mta_command_completion (char *cmd, int start, int end)
{
  if (start == 0)
    return rl_completion_matches (cmd, mta_command_generator);
  return NULL;
}
#endif

char *mta_capa[] = {
#ifdef HAVE_TLS
  "STARTTLS",
#endif
  NULL
};

void
reset_capa (char *name)
{
  int i;
  for (i = 0; mta_capa[i]; i++)
    if (strcmp (mta_capa[i], name) == 0)
      {
	mta_capa[i] = NULL;
	break;
      }
}

static int tempfail;
static int discard;
static char *nullmailer;
unsigned nrcpt = 0;
unsigned nbadrcpts = 0;

void
smtp_ehlo (int extended, char *domain)
{
  int i;

  if (gsrv)
    {
      gacopyz_srv_define_macro (gsrv, "s", domain);
      switch (gacopyz_srv_helo (gsrv, domain))
	{
	case SMFIR_REPLYCODE:
	  {
	    if (gacopyz_srv_reply (gsrv, &nullmailer, NULL))
	      {
		if (errno == ENOMEM)
		  mu_alloc_die ();
		else
		  {
		    mu_error (_("%s:%d: INTERNAL ERROR: no reply (%s)"),
			      __FILE__, __LINE__, mu_strerror (errno));
		    abort ();
		  }
	      }
	    break;
	  }

	case SMFIR_REJECT:
	  nullmailer = mu_strdup ("Command rejected");
	  break;

	case SMFIR_TEMPFAIL:
	  tempfail = 1;
	  break;
	  
	case SMFIR_DISCARD:
	case SMFIR_SHUTDOWN:
	  break;
	}
    }
  
  if (!extended)
    {
      smtp_reply (250, "pleased to meet you");
      return;
    }

  smtp_reply (R_CONT | 250, "pleased to meet you");
  for (i = 0; mta_capa[i]; i++)
    smtp_reply (R_CONT | 250, "%s", mta_capa[i]);
  smtp_reply (250, "HELP");
}

void
smtp_help (void)
{
  int i;
  
  smtp_reply (250|R_CONT, "mtasim (%s); supported SMTP commands:", PACKAGE_STRING);
  for (i = 0; kw[i].name; i++)
    smtp_reply (250|R_CONT, " %s", kw[i].name);
  if (milter_port)
    {
      smtp_reply (250|R_CONT, "Supported administrative commands:");
      shell_help ();
    }
  smtp_reply (250, "End of help output");
}

#define MSG_TEMPFAIL "451 4.3.2 Please try again later"
#define MSG_REJECT "550 5.7.1 Command rejected"
#define MSG_SHUTDOWN "421 4.7.0 bitbucket closing connection"

int
parse_email_addr (char *arg, char **psender, char **addr, char **host)
{
  size_t len;
  char *p = arg, *q;
  if (*p == '<')
    {
      len = strlen (p);
      if (p[len-1] != '>')
	return 1;
      p++;
      *psender = mu_alloc (len - 1);
      if (*psender)
	{
	  memcpy (*psender, p, len - 2);
	  (*psender)[len - 2] = 0;
	}
    }
  else
    *psender = mu_strdup (arg);
  if (!*psender)
    return 1;
  p = *psender;
  q = strchr (p, '@');
  if (q)
    len = q - p;
  else
    len = strlen (p);
  *addr = mu_alloc (len + 1);
  if (!*addr)
    {
      free (*psender);
      return 1;
    }
  memcpy (*addr, p, len);
  (*addr)[len] = 0;
  if (q)
    q++;
  else
    q = "localhost";
  *host = mu_strdup (q);
  if (!*host)
    {
      free (*psender);
      free (*addr);
      return 1;
    }
  return 0;
}

static int
process_gacopyz_reply (char *sname, char *arg, int rc, int *state)
{
  switch (rc)
    {
    case SMFIR_REPLYCODE:
      {
	char *msg;
	if (gacopyz_srv_reply (gsrv, &msg, NULL))
	  {
	    if (errno == ENOMEM)
	      mu_alloc_die ();
	    else
	      {
		mu_error (_("%s:%d: INTERNAL ERROR: no reply (%s)"),
			  __FILE__, __LINE__, mu_strerror (errno));
		abort ();
	      }
	  }
			
	if (verbose)
	  fprintf (stderr,
		   "Gacopyz: %s=%s, reply=%s",
		   sname, arg, msg);
	smtp_send_string (msg, 0);
	free (msg);
	return 1;
      }

    case SMFIR_REJECT:
      if (verbose)
	fprintf (stderr, 
		 "Gacopyz: %s=%s, reply=%s",
		 sname, arg, MSG_REJECT);
      smtp_send_string (MSG_REJECT, 0);
      return 1;

    case SMFIR_DISCARD:
      if (verbose)
	fprintf (stderr, 
		 "Gacopyz: %s=%s, discard",
		 sname, arg);
      discard = 1;
      return 1;

    case SMFIR_TEMPFAIL:
      if (verbose)
	fprintf (stderr, 
		 "Gacopyz: %s=%s, reply=%s",
		 sname, arg, MSG_TEMPFAIL);
      smtp_send_string (MSG_TEMPFAIL, 0);
      return 1;

    case SMFIR_SHUTDOWN:
      if (verbose)
	fprintf (stderr, 
		 "Gacopyz: %s=%s, reply=%s",
		 sname, arg, MSG_SHUTDOWN);
      smtp_send_string (MSG_SHUTDOWN, 0);
      *state = STATE_QUIT;
      return 1;
    }
  return 0;
}

static int
process_data_reply (char *sname, char *arg, int rc, int *state, char **reply)
{
  switch (rc)
    {
    case SMFIR_REPLYCODE:
      {
	if (gacopyz_srv_reply (gsrv, reply, NULL))
	  {
	    if (errno == ENOMEM)
	      mu_alloc_die ();
	    else
	      {
		mu_error (_("%s:%d: INTERNAL ERROR: no reply (%s)"),
			  __FILE__, __LINE__, mu_strerror (errno));
		abort ();
	      }
	  }
			
	if (verbose)
	  fprintf (stderr,
		   "Gacopyz: %s=%s, reply=%s",
		   sname, arg, *reply);
	return 1;
      }

    case SMFIR_REJECT:
      if (verbose)
	fprintf (stderr, 
		 "Gacopyz: %s=%s, reply=%s",
		 sname, arg, MSG_REJECT);
      *reply = mu_strdup (MSG_REJECT);
      return 1;

    case SMFIR_DISCARD:
      if (verbose)
	fprintf (stderr, 
		 "Gacopyz: %s=%s, discard",
		 sname, arg);
      discard = 1;
      return 1;

    case SMFIR_TEMPFAIL:
      if (verbose)
	fprintf (stderr, 
		 "Gacopyz: %s=%s, reply=%s",
		 sname, arg, MSG_TEMPFAIL);
      *reply = mu_strdup (MSG_TEMPFAIL);
      return 1;

    case SMFIR_SHUTDOWN:
      if (verbose)
	fprintf (stderr, 
		 "Gacopyz: %s=%s, reply=%s",
		 sname, arg, MSG_SHUTDOWN);
      smtp_send_string (MSG_SHUTDOWN, 0);
      *state = STATE_QUIT;
      return 1;
    }
  return 0;
}

/* Check if ws->ws_wordv[1] begins with the string PFX, followed by ':'.
   On success, return index of the first word after 'PFX:', removing the
   prefix if necessary.
   Return 0 otherwise.
*/
size_t 
check_address_command (const char *pfx, struct mu_wordsplit *ws)
{
  int pfxlen = strlen (pfx);
  int arglen = strlen (ws->ws_wordv[1]);
  
  if (ws->ws_wordc >= 2 && arglen > pfxlen
      && strncasecmp (ws->ws_wordv[1], pfx, pfxlen) == 0
      && ws->ws_wordv[1][pfxlen] == ':') {
    if (arglen == pfxlen + 1)
      return 2;
    else
      {
	memmove (ws->ws_wordv[1], ws->ws_wordv[1] + pfxlen + 1,
		 arglen - pfxlen);
	return 1;
      }
  }
  return 0;
}

static void
smtp_mail (struct mu_wordsplit *ws, int *state)
{
  size_t n;

  n = check_address_command ("from", ws);
  if (n == 0)
    {
      smtp_reply (501, "Syntax error");
    }
  else
    {
      int rc = 0;
      char *sender;
      char *addr;
      char *host;
      
      if (parse_email_addr (ws->ws_wordv[n], &sender, &addr, &host))
	{
	  smtp_reply (553, "5.0.0 sender address syntactically incorrect");
	  return;
	}
	      
      if (gsrv)
	{
	  gacopyz_srv_define_macro (gsrv, "f", sender);
	  gacopyz_srv_define_macro (gsrv, "r", "SMTP");
	  /* FIXME: 
	     gacopyz_srv_define_macro (gsrv, "s", host);
	  */
	  gacopyz_srv_define_macro (gsrv, "ntries", "0");
	  gacopyz_srv_define_macro (gsrv, "nrcpts", "0");
	  gacopyz_srv_define_macro (gsrv, "nbadrcpts", "0");

	  /* FIXME */
	  gacopyz_srv_define_macro (gsrv, "mail_mailer", "local");
	  gacopyz_srv_define_macro (gsrv, "mail_host", host);
	  gacopyz_srv_define_macro (gsrv, "mail_addr", addr);
	  gacopyz_srv_define_macro (gsrv, "mail_from", sender);
	  
	  rc = gacopyz_srv_envfrom (gsrv, ws->ws_wordv + n);
	  rc = process_gacopyz_reply ("from", ws->ws_wordv[n], rc, state);
	}

      free (sender);
      free (addr);
      free (host);

      if (rc == 0)
	{
	  smtp_reply (250, "Sender OK");
	  *state = STATE_MAIL;
	}
    }
}

static void
smtp_rcpt (struct mu_wordsplit *ws, int *state)
{
  size_t n;
  
  nrcpt++;
  n = check_address_command ("to", ws);
  if (n == 0)
    {
      smtp_reply (501, "Syntax error");
    }
  else
    {
      int rc = 0;
      char *sender;
      char *addr;
      char *host;
	      
      if (parse_email_addr (ws->ws_wordv[n], &sender, &addr, &host))
	{
	  smtp_reply (553, "5.0.0 recipient address syntactically incorrect");
	  nbadrcpts++;
	  return;
	}
	      
      if (gsrv)
	{
	  update_nrcpts ("nrcpts", nrcpt);
	  update_nrcpts ("nbadrcpts", nbadrcpts);
	  gacopyz_srv_define_macro (gsrv, "rcpt_host", host);
	  gacopyz_srv_define_macro (gsrv, "rcpt_addr", addr);
	  
	  rc = gacopyz_srv_envrcpt (gsrv, ws->ws_wordv + n);
	  rc = process_gacopyz_reply ("to", ws->ws_wordv[n], rc, state);
	}

      free (sender);
      free (addr);
      free (host);

      if (rc == 0)
	{
	  smtp_reply (250, "Recipient OK");
	  *state = STATE_RCPT;
	}
    }
}

int
process_header (mu_opool_t pool, int *state, char **reply)
{
  int status = 0;
  char *hn, *hv;
  
  mu_opool_append_char (pool, 0);
  hn = mu_opool_finish (pool, NULL);
  hv = strchr (hn, ':');
  if (hv)
    {
      int rc, len;
      
      *hv++ = 0;
      while (*hv && mu_isspace (*hv))
	hv++;
      len = strlen (hv);
      if (len > 0 && hv[len - 1] == '\n')
	{
	  if (--len > 0 && hv[len - 1] == '\r')
	    len--;
	  hv[len] = 0;
	}
      rc = gacopyz_srv_header (gsrv, hn, hv);
      status = process_data_reply ("cmd", "header", rc, state, reply);
    }
  mu_opool_free (pool, hn);
  return status;
}

struct body_buf
{
  char *bufptr;
  size_t level;
};

int
send_body (int state, char *ptr, size_t len, struct body_buf *buf)
{
  while (len > 0)
    {
      size_t s = max_body_chunk - buf->level;
      if (s > len)
	s = len;
      memcpy (buf->bufptr + buf->level, ptr, s);
      ptr += s;
      len -= s;
      buf->level += s;
      if (buf->level == max_body_chunk)
	{
	  char *datareply = NULL;
	  int rc = gacopyz_srv_body (gsrv, (unsigned char*) buf->bufptr,
				     buf->level);
	  buf->level = 0;
	  if (process_data_reply ("cmd", "data", rc, &state, &datareply))
	    {
	      if (datareply)
		{
		  smtp_send_string (datareply, 0);
		  free (datareply);
		  datareply = 0;
		}
	    }
	}
    }
  return state;
}

void
smtp (void)
{
  int state;
  char buf[128];
  mu_opool_t pool = NULL;
  char *datareply = NULL;
  struct body_buf body_buf;
  struct mu_wordsplit ws;
  int wsflags;
  
  body_buf.bufptr = mu_alloc (max_body_chunk);
  body_buf.level = 0;

  if (milter_port)
    {
      smtp_reply (220|R_CONT, "mtasim (%s) ready", PACKAGE_STRING);
      smtp_reply (220, "Connected to milter %s", milter_port);
    }
  else
    smtp_reply (220, "mtasim (%s) ready", PACKAGE_STRING);

  wsflags = MU_WRDSF_NOVAR
          | MU_WRDSF_NOCMD
          | MU_WRDSF_SQUEEZE_DELIMS
          | MU_WRDSF_SHOWERR
          | MU_WRDSF_ENOMEMABRT
          | MU_WRDSF_WS;
  
  for (state = STATE_INIT; state != STATE_QUIT;)
    {
      int kw, len;
      
      if (get_input_line (buf, sizeof buf) <= 0)
	{
	  state = STATE_QUIT;
	  continue;
	}

      len = strlen (buf);

      if (trace)
	fprintf (trace, "%s", buf);

      if (state != STATE_HEADERS && state != STATE_DATA)
	{
	  mu_rtrim_class (buf, MU_CTYPE_ENDLN);
	  mu_wordsplit (buf, &ws, wsflags);
	  wsflags |= MU_WRDSF_REUSE;
	  if (ws.ws_wordc == 0)
	    continue;
      
	  if (ws.ws_wordv[0][0] == '\\' && milter_port)
	    {
	      shell (ws.ws_wordc, ws.ws_wordv);
	      continue;
	    }
	  
	  kw = smtp_kw (ws.ws_wordv[0]);

	  if (kw != KW_HELP && !sender_hostname)
	    {
	      sender_hostname = mu_strdup ("localhost");
	      if (gsrv)
		gacopyz_srv_connect (gsrv, sender_hostname, NULL);
	    }
	  
	  switch (kw)
	    {
	    case KW_QUIT:
	      state = STATE_QUIT;
	      smtp_reply (221, "Done");
	      continue;
	      
	    case KW_HELP:
	      smtp_help ();
	      continue;
	      
	    case KW_RSET:
	      if (gsrv)
		gacopyz_srv_abort (gsrv);
	      state = STATE_INIT;
	      free (nullmailer);
	      nullmailer = NULL;
	      tempfail = 0;
	      nrcpt = 0;
	      nbadrcpts = 0;
	      discard = 0;
	      free (datareply);
	      datareply = NULL;
	      if (pool)
		mu_opool_destroy (&pool);
	      smtp_reply(250, "2.0.0 Reset state");
	      continue;
	    }
	}
      else if (len > 0 && buf[len - 1] == '\n' && buf[len - 2] == '\r')
	{
	  buf[len - 2] = '\n';
	  len--;
	}
      
      buf[len] = 0;
      if (state != STATE_INIT)
	{
	  if (nullmailer)
	    {
	      smtp_send_string (nullmailer, 0);
	      continue;
	    }
	  if (tempfail)
	    {
	      smtp_send_string (MSG_TEMPFAIL, 0);
	      continue;
	    }
	}
	    
      switch (state) {
      case STATE_INIT:
	switch (kw) {
	case KW_EHLO:
	case KW_HELO:
	  if (ws.ws_wordc == 2)
	    {
	      smtp_ehlo (kw == KW_EHLO, ws.ws_wordv[1]);
	      state = STATE_EHLO;
	    }
	  else
	    smtp_reply (501, "%s requires domain address", ws.ws_wordv[0]);
	  break;
	  
	default:
	  smtp_reply (503, "Polite people say HELO first");
	  break;
	}
	break;
	
      case STATE_EHLO:
	switch (kw) {
	case KW_EHLO:
	  if (ws.ws_wordc == 2)
	    {
	      smtp_ehlo (1, ws.ws_wordv[1]);
	    }
	  else
	    smtp_reply (501, "%s requires domain address", ws.ws_wordv[0]);
	  break;

	case KW_MAIL:
	  smtp_mail (&ws, &state);
	  break;

#ifdef HAVE_TLS
	case KW_STARTTLS:
	  smtp_starttls ();
	  break;
#endif
	default:
	  smtp_reply (503, "Need MAIL command");
	}
	break;
	
      case STATE_MAIL:
	switch (kw) {
	case KW_MAIL:
	  smtp_reply (503, "5.5.0 Sender already specified");
	  break;
	  
	case KW_RCPT:
	  smtp_rcpt (&ws, &state);
	  break;
	  
	default:
	  smtp_reply (503, "Need RCPT command");
	}
	break;
	
      case STATE_RCPT:
	switch (kw) {
	case KW_RCPT:
	  smtp_rcpt (&ws, &state);
	  break;
	  
	case KW_DATA:
	  if (gsrv)
	    {
	      int rc = gacopyz_srv_data (gsrv);
	      if (process_gacopyz_reply ("cmd", "data", rc, &state))
		continue;
	    }

	  smtp_reply (354,
		      "Enter mail, end with \".\" on a line by itself");
	  if (!pool)
	    mu_opool_create (&pool, MU_OPOOL_ENOMEMABRT);
	  state = STATE_HEADERS;
	  break;
	  
	default:
	  smtp_reply (501, "Syntax error");
	}
	break;
	
      case STATE_HEADERS:
	if (strcmp (buf, "\n") == 0)
	  {
	    if (gsrv)
	      {
		int rc;
		
		if (pool && mu_opool_head (pool, NULL))
		  {
		    if (process_header (pool, &state, &datareply))
		      continue;
		  }
		rc = gacopyz_srv_eoh (gsrv);
		if (process_data_reply ("cmd", "eoh", rc, &state, &datareply))
		  continue;
	      }
	    body_buf.level = 0;
	    state = STATE_DATA;
	  }
	else if (buf[0] == ' ' || buf[0] == '\t')
	  {
	    if (gsrv)
	      {
		mu_opool_append (pool, buf, len - 1);
		mu_opool_appendz (pool, "\r\n");
	      }
	  }
	else if (strcmp (buf, ".") == 0)
	  {
	    if (gsrv)
	      {
		int rc = gacopyz_srv_eom (gsrv, NULL, 0);
		process_data_reply ("cmd", "eom", rc, &state, &datareply);
		/* FIXME: Clear macro table, except for the entries from
		   command line */
		gacopyz_srv_clear_macros (gsrv);
	      }
	    if (datareply)
	      {
		smtp_send_string (datareply, 0);
		free (datareply);
		datareply = 0;
	      }
	    else
	      smtp_reply (250, "Mail accepted for delivery");
	    state = STATE_EHLO;
 	  }
	else if (gsrv)
	  {
	    if (pool && mu_opool_head (pool, NULL))
	      {
		if (process_header (pool, &state, &datareply))
		  continue;
	      }
	    mu_opool_append (pool, buf, len - 1);
	    mu_opool_appendz (pool, "\r\n");
	  }
	break;
	
      case STATE_DATA:
	if (strcmp (buf, ".\n") == 0)
	  {
	    if (gsrv)
	      {
		int rc;

		rc = gacopyz_srv_eom (gsrv, (unsigned char *)body_buf.bufptr,
				      body_buf.level);
		process_data_reply ("cmd", "eom", rc, &state, &datareply);
		/* FIXME: Clear macro table, except for the entries from
		   the command line */
		gacopyz_srv_clear_macros (gsrv);
	      }
	    if (datareply)
	      {
		smtp_send_string (datareply, 0);
		free (datareply);
		datareply = 0;
	      }
	    else
	      smtp_reply (250, "Mail accepted for delivery");
	    state = STATE_EHLO;
	  }
	else if (gsrv)
	  {
	    state = send_body (state, buf, len - 1, &body_buf);
	    if (state == STATE_DATA)
	      state = send_body (state, "\r\n", 2, &body_buf);
	  }
	break;
      }
    }
  if (wsflags & MU_WRDSF_REUSE)
    mu_wordsplit_free (&ws);
  if (pool)
    mu_opool_destroy (&pool);
  if (gsrv)
    {
      gacopyz_srv_quit (gsrv);
      gacopyz_srv_close (gsrv);
      gacopyz_srv_destroy (&gsrv);
    }
  free (body_buf.bufptr);
}

int
mta_daemon ()
{
  int on = 1;
  struct sockaddr_in address;
  int fd;

  fd = socket (PF_INET, SOCK_STREAM, 0);
  if (fd < 0)
    {
      perror ("socket");
      return 1;
    }

  setsockopt (fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof (on));

  memset (&address, 0, sizeof (address));
  address.sin_family = AF_INET;
  address.sin_addr.s_addr = INADDR_ANY;

  if (port)
    address.sin_port = htons (port);
  else
    address.sin_port = 0;
  
  if (bind (fd, (struct sockaddr *) &address, sizeof (address)) < 0)
    {
      close (fd);
      perror ("bind");
      return 1;
    }
  if (!port)
    {
      socklen_t len = sizeof (address);
      int rc = getsockname (fd, (struct sockaddr *) &address, &len);
      if (rc)
	{
	  close (fd);
	  mu_error ("getsockname: %s", mu_strerror (errno));
	  return 1;
	}
      port = ntohs (address.sin_port);
      printf ("%d\n", port);
      fclose (stdout);
    }

  listen (fd, 5);
  while (1)
    {
      fd_set rfds;
      struct sockaddr_in his_addr;
      int sfd, status;
      socklen_t len;

      FD_ZERO (&rfds);
      FD_SET (fd, &rfds);

      status = select (fd + 1, &rfds, NULL, NULL, NULL);
      if (status == -1)
	{
	  if (errno == EINTR)
	    continue;
	  perror ("select");
	  return 1;
	}

      len = sizeof (his_addr);
      if ((sfd = accept (fd, (struct sockaddr *) &his_addr, &len)) < 0)
	{
	  perror ("accept");
	  return 1;
	}

      in = out = &fd;
      smtp ();
      break;
    }

  return 0;
}

int
mta_stdio ()
{
  int ifd = fileno (stdin), ofd = fileno (stdout);
  in = &ifd;
  out = &ofd;
  smtp ();
  return 0;
}



int
define_macro (char *arg)
{
  char *p;
  p = strchr (arg, '=');
  if (!p)
    return 1;
  *p++ = 0;
  gacopyz_srv_define_macro (gsrv, arg, p);
  return 0;
}

void
undefine_macro (char *arg)
{
  gacopyz_srv_del_macro (gsrv, arg);
}

void
undefine_all_macros ()
{
  gacopyz_srv_clear_macros (gsrv);
}

void
list_macro (char *name)
{
  const char *val;
  if (gacopyz_srv_find_macro (gsrv, name, &val) == MI_SUCCESS)
    smtp_reply (220, "%s=%s", name, val);
  else
    smtp_reply (220, "%s undefined", name);
}

struct macro_itr
{
  size_t num;
  size_t count;
};

int
macprint (const char *name, const char *value, void *data)
{
  struct macro_itr *itr = data;
  int code = 220;
  
  itr->num++;
  if (itr->num < itr->count)
    code |= R_CONT;
  smtp_reply (code, "%s=%s", name, value);
  return 0;
}

void
list_all_macros ()
{
  struct macro_itr itr;

  itr.num = itr.count = 0;
  gacopyz_srv_count_macros (gsrv, &itr.count);
  
  if (itr.count == 0)
    smtp_reply (220, "No macros defined");
  else
    gacopyz_srv_iterate_macros (gsrv, macprint, &itr);
}

static void
_sockaddr_error_shell (void *data, const char *msg, const char *arg)
{
  if (arg)
    smtp_reply (502, "%s: %s", msg, arg);
  else
    smtp_reply (502, "%s", msg);
}


/* \Sfamily hostname address [port] */
void
set_sender_socket (int argc, char **argv)
{
  if (sender_hostname)
    smtp_reply (502, "Sender sockaddr already set");
  else
    {
      int reset = 0;

      if (strcmp (argv[0], "\\S") == 0)
	{
	  ++argv;
	  --argc;
	}
      else
	{
	  argv[0] += 2;
	  reset = 1;
	}
      
      if (convert_sender_sockaddr (argc, argv,
				   _sockaddr_error_shell, NULL) == 0)
	gacopyz_srv_connect (gsrv, sender_hostname, sender_sockaddr);
      if (reset)
	argv[0] -= 2;
    }
}

void
shell_help ()
{
  static char *hstr[] = {
    " \\Dname=value [name=value...]       Define Sendmail macros",
    " \\Ecode                             Expect given SMTP reply code",
    " \\L[name] [name...]                 List macros",
    " \\Uname [name...]                   Undefine Sendmail macros",
    " \\Sfamily hostname address [port]   Define sender socket address",
    " \\Wseconds[.fraq]                   Wait given number of seconds",
    " \\#text                             Comment",
  };
  int hcount = sizeof (hstr) / sizeof (hstr[0]);
  int i;

  if (sender_hostname)
    hcount--;
  for (i = 0; i < hcount; i++)
    smtp_reply (((i < hcount-1) ? R_CONT : 0) | 250, "%s", hstr[i]);
}

void
shell (int argc, char **argv)
{
  switch (argv[0][1])
    {
    case '#':
      break;

    case 'd':
    case 'D':
      {
	int rc = 0;
	if (argv[0][2])
	  rc = define_macro (&argv[0][2]);
	while (rc == 0 && --argc)
	  rc = define_macro (*++argv);

	if (rc)
	  smtp_reply (502, "Malformed administrative command");
      }
      break;

    case 'u':
    case 'U':
      if (argv[0][2])
	{
	  undefine_macro (&argv[0][2]);
	  while (--argc)
	    undefine_macro (*++argv);
	}
      else if (argc > 1)
	while (--argc)
	  undefine_macro (*++argv);
      else
	undefine_all_macros ();
      break;

    case 'l':
    case 'L':
      if (argv[0][2])
	{
	  list_macro (&argv[0][2]);
	  while (--argc)
	    list_macro (*++argv);
	}
      else if (argc > 1)
	while (--argc)
	  list_macro (*++argv);
      else
	list_all_macros ();
      break;

    case 'e':
    case 'E':
      if (set_expected_code (argv[0] + 2))
	smtp_reply (502, "Invalid SMPT code: %s", argv[0] + 2);
      break;

    case 's':
    case 'S': 
      /* \Sfamily hostname address [port] */
      set_sender_socket (argc, argv);
      break;

    case 'w':
    case 'W':
      /* \Wseconds */
      {
	struct timeval tv;
	if (strtotv (argv[0] + 2, NULL, &tv) == 0)
	  {
	    select (0, NULL, NULL, NULL, &tv);
	  }
	else
	  smtp_reply (502, "Bad time to wait");
      }
      break;
      
    case '?':
      shell_help ();
      break;
      
    default:
      smtp_reply (502, "Unknown administrative command");
      break;
    }
}

/*
 Local Variables:
 c-file-style: "gnu"
 End:
*/
/* EOF */
