/* GNU Mailutils -- a suite of utilities for electronic mail
   Copyright (C) 1999-2025 Free Software Foundation, Inc.

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 3 of the License, or (at your option) any later version.

   This library 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
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General
   Public License along with this library.  If not, see
   <http://www.gnu.org/licenses/>. */

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

#ifdef ENABLE_SMTP

#include <errno.h>
#include <netdb.h>
#include <pwd.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>

#include <mailutils/argcv.h>
#include <mailutils/nls.h>
#include <mailutils/address.h>
#include <mailutils/wordsplit.h>
#include <mailutils/debug.h>
#include <mailutils/errno.h>
#include <mailutils/header.h>
#include <mailutils/body.h>
#include <mailutils/iterator.h>
#include <mailutils/message.h>
#include <mailutils/mime.h>
#include <mailutils/util.h>
#include <mailutils/observer.h>
#include <mailutils/property.h>
#include <mailutils/stream.h>
#include <mailutils/smtp.h>
#include <mailutils/tls.h>
#include <mailutils/cstr.h>
#include <mailutils/sockaddr.h>
#include <mailutils/sys/mailer.h>
#include <mailutils/sys/url.h>
#include <mailutils/sys/registrar.h>

static int      _mailer_smtp_init (mu_mailer_t);

static int
_url_smtp_init (mu_url_t url)
{
  if (url->port == 0)
    url->port = MU_SMTP_PORT;
  return 0;
}

static struct _mu_record _smtp_record = {
  MU_SMTP_PRIO,
  MU_SMTP_SCHEME,
  MU_RECORD_DEFAULT,
  MU_URL_SCHEME | MU_URL_CRED | MU_URL_INET | MU_URL_PARAM,
  MU_URL_HOST,
  _url_smtp_init,		/* url init.  */
  _mu_mailer_mailbox_init,	/* Mailbox init.  */
  _mailer_smtp_init,		/* Mailer init.  */
  _mu_mailer_folder_init,	/* Folder init.  */
  NULL,				/* No need for a back pointer.  */
  NULL,				/* _is_scheme method.  */
  NULL,				/* _get_url method.  */
  NULL,				/* _get_mailbox method.  */
  NULL,				/* _get_mailer method.  */
  NULL				/* _get_folder method.  */
};

/* We export : url parsing and the initialization of
   the mailbox, via the register entry/record.  */
mu_record_t     mu_smtp_record = &_smtp_record;

#ifdef WITH_TLS
static struct _mu_record _smtps_record = {
  MU_SMTP_PRIO,
  MU_SMTPS_SCHEME,
  MU_RECORD_DEFAULT,
  MU_URL_SCHEME | MU_URL_CRED | MU_URL_INET | MU_URL_PARAM,
  MU_URL_HOST,
  _url_smtp_init,		/* url init.  */
  _mu_mailer_mailbox_init,	/* Mailbox init.  */
  _mailer_smtp_init,		/* Mailer init.  */
  _mu_mailer_folder_init,	/* Folder init.  */
  NULL,				/* No need for a back pointer.  */
  NULL,				/* _is_scheme method.  */
  NULL,				/* _get_url method.  */
  NULL,				/* _get_mailbox method.  */
  NULL,				/* _get_mailer method.  */
  NULL				/* _get_folder method.  */
};
mu_record_t     mu_smtps_record = &_smtps_record;
#endif

enum mailer_tls
  {
    MAILER_TLS_NONE,
    MAILER_TLS_ALWAYS,
    MAILER_TLS_STARTTLS
  };

struct _smtp_mailer
{
  mu_mailer_t     mailer;
  mu_smtp_t       smtp;
  int             strip_domain; /* Strip domain part from
				   recipients' emails. */
  char const      *add_domain;  /* Add this domain to emails with missing
				   domain part. */
  char           **rcpt_hdr;    /* NULL-terminated list of headers to read
				   recipients from. */
  mu_address_t    rcpt;

  enum mailer_tls tls;
  int auth:1;
};

static void
smtp_mailer_add_auth_mech (struct _smtp_mailer *smtp_mailer, const char *str)
{
  struct mu_wordsplit ws;

  ws.ws_delim = ",";
  if (mu_wordsplit (str, &ws, MU_WRDSF_DEFFLAGS|MU_WRDSF_DELIM|MU_WRDSF_WS))
    mu_error (_("cannot split line %s: %s"), mu_quotename (str),
	      mu_wordsplit_strerror (&ws));
  else
    {
      size_t i;
      for (i = 0; i < ws.ws_wordc; i++)
	mu_smtp_add_auth_mech (smtp_mailer->smtp, ws.ws_wordv[i]);
      mu_wordsplit_free (&ws);
    }
}

/* FIXME: So far, ESMTP AUTH support is available only if MU is compiled
   with GNU SASL. Should this change, the define below must be changed
   as well. See also smtp_auth.c.
 */
#ifdef WITH_GSASL
# define DFLAUTH 1
#else
# define DFLAUTH 0
#endif

static char default_rcpt_hdr[] = "to,cc,bcc";

/* Create the mu_smtp_t object and initialize the _smtp_mailer in
   accordance with the URL of the mailer.  This initialization cannot
   be done in _mailer_smtp_init, because at that stage mailer->url is
   not yet known.
 */
static int
_mailer_smtp_init_late (mu_mailer_t mailer)
{
  struct _smtp_mailer *smp = mailer->data;
  const char *auth, *scheme;
  int rc;
  size_t parmc = 0;
  char **parmv = NULL;
  char const *rcpt_hdr = NULL;

  if (smp->smtp)
    return 0;
  
  smp->auth = DFLAUTH;
  
  rc = mu_url_sget_scheme (mailer->url, &scheme);
  if (rc == 0 && strcmp (scheme, "smtps") == 0) 
    smp->tls = MAILER_TLS_ALWAYS; 
  else
    smp->tls = MAILER_TLS_STARTTLS;

  rc = mu_smtp_create (&smp->smtp);
  if (rc)
    return rc;
  if (mu_debug_level_p (MU_DEBCAT_MAILER, MU_DEBUG_PROT))
    mu_smtp_trace (smp->smtp, MU_SMTP_TRACE_SET);
  if (mu_debug_level_p (MU_DEBCAT_MAILER, MU_DEBUG_TRACE6))
    mu_smtp_trace_mask (smp->smtp, MU_SMTP_TRACE_SET,
			MU_XSCRIPT_SECURE);
  if (mu_debug_level_p (MU_DEBCAT_MAILER, MU_DEBUG_TRACE7))
    mu_smtp_trace_mask (smp->smtp, MU_SMTP_TRACE_SET,
			MU_XSCRIPT_PAYLOAD);
  
  mu_smtp_set_url (smp->smtp, mailer->url);

  if (mu_url_sget_auth (mailer->url, &auth) == 0)
    smtp_mailer_add_auth_mech (smp, auth);

  rcpt_hdr = default_rcpt_hdr;
  
  /* Additional information is supplied via parameters. */
  if (mu_url_sget_fvpairs (mailer->url, &parmc, &parmv) == 0)
    {
      size_t i;
      
      for (i = 0; i < parmc; i++)
	{
	  if (strcmp (parmv[i], "notls") == 0)
	    smp->tls = MAILER_TLS_NONE;
	  else if (strcmp (parmv[i], "noauth") == 0)
	    smp->auth = 0;
	  else if (strncmp (parmv[i], "auth=", 5) == 0)
	    smtp_mailer_add_auth_mech (smp, parmv[i] + 5);
	  else if (strncmp (parmv[i], "ehlo=", 5) == 0)
	    mu_smtp_set_param (smp->smtp, MU_SMTP_PARAM_DOMAIN, parmv[i] + 5);
	  else if (strcmp (parmv[i], "strip-domain") == 0)
	    smp->strip_domain = 1;
	  else if (strncmp (parmv[i], "domain=", 7) == 0)
	    smp->add_domain = parmv[i] + 7;
	  else if (strcmp (parmv[i], "recipient-headers") == 0)
	    rcpt_hdr = NULL;
	  else if (strncmp (parmv[i], "recipient-headers=", 18) == 0)
	    rcpt_hdr = parmv[i] + 18;
	  else if (strcmp (parmv[i], "read-recipients") == 0)
	    {
	      mu_property_t property = NULL;
	      mu_mailer_get_property (smp->mailer, &property);
	      mu_property_set (property, "READ_RECIPIENTS");
	    }
	  else if (strcmp (parmv[i], "from") == 0 ||
		   strcmp (parmv[i], "to") == 0)
	    /* handled elsewhere */;
	  else
	    mu_debug (MU_DEBCAT_MAILER, MU_DEBUG_ERROR,
		      ("unhandled parameter in SMTP URL: %s", parmv[i]));
	}

      if (rcpt_hdr)
	{
	  int rc;
	  size_t n;
	  struct mu_wordsplit ws;
	  ws.ws_delim = ",";
	  rc = mu_wordsplit (rcpt_hdr, &ws,
			     MU_WRDSF_DELIM|WRDSF_NOCMD|WRDSF_NOVAR);
	  if (rc)
	    rc = errno;
	  else if (wordsplit_get_words (&ws, &n, &smp->rcpt_hdr))
	    rc = ENOMEM;
	  mu_wordsplit_free (&ws);
	  if (rc)
	    return rc;
	}
      else
	smp->rcpt_hdr = NULL;
    }

  return 0;
}

static int
smtp_open (mu_mailer_t mailer, int flags)
{
  struct _smtp_mailer *smtp_mailer = mailer->data;
  int rc;
  struct mu_sockaddr *sa;
  struct mu_sockaddr_hints hints;
  mu_stream_t transport;

  rc = _mailer_smtp_init_late (mailer);
  if (rc)
    return rc;
  
  memset (&hints, 0, sizeof (hints));
  hints.flags = MU_AH_DETECT_FAMILY;
  hints.port = smtp_mailer->tls == MAILER_TLS_ALWAYS ? MU_SMTPS_PORT : MU_SMTP_PORT;
  hints.protocol = IPPROTO_TCP;
  hints.socktype = SOCK_STREAM;
  rc = mu_sockaddr_from_url (&sa, mailer->url, &hints);
  if (rc)
    return rc;
      
  rc = mu_tcp_stream_create_from_sa (&transport, sa, NULL, mailer->flags);
  if (rc)
    {
      mu_sockaddr_free (sa);
      return rc;
    }
    
#ifdef WITH_TLS
  if (smtp_mailer->tls == MAILER_TLS_ALWAYS)
    {
      mu_stream_t tlsstream;

      rc = mu_tlsfd_stream_convert (&tlsstream, transport, NULL,
				    MU_TLS_CLIENT);
      mu_stream_unref (transport);
      if (rc)
	{
	  if (rc == MU_ERR_TRANSPORT_SET)
	    {
	      mu_stream_destroy (&tlsstream);
	      mu_tls_enable = 0;
	    }
	  mu_debug (MU_DEBCAT_FOLDER, MU_DEBUG_ERROR,
		    ("cannot create TLS stream: %s", mu_strerror (rc)));
	  if (mu_tls_enable)
	    return rc;
	}
      transport = tlsstream;
    }
#endif
  mu_stream_set_buffer (transport, mu_buffer_line, 0);
  mu_smtp_set_carrier (smtp_mailer->smtp, transport);
  mu_stream_unref (transport);

  rc = mu_smtp_open (smtp_mailer->smtp);
  if (rc)
    return rc;

  rc = mu_smtp_ehlo (smtp_mailer->smtp);
  if (rc)
    return rc;

#ifdef WITH_TLS
  if (smtp_mailer->tls == MAILER_TLS_STARTTLS &&
      mu_smtp_capa_test (smtp_mailer->smtp, "STARTTLS", NULL) == 0)
    {
      rc = mu_smtp_starttls (smtp_mailer->smtp);
      if (rc == 0)
	{
	  rc = mu_smtp_ehlo (smtp_mailer->smtp);
	  if (rc)
	    return rc;
	}
    }
#endif
  if (smtp_mailer->auth &&
      mu_smtp_capa_test (smtp_mailer->smtp, "AUTH", NULL) == 0)
    {
      rc = mu_smtp_auth (smtp_mailer->smtp);
      switch (rc)
	{
	case 0:
	  rc = mu_smtp_ehlo (smtp_mailer->smtp);
	  break;
	  
	case ENOSYS:
	case MU_ERR_AUTH_NO_CRED:
	  mu_diag_output (MU_DIAG_NOTICE, "authentication disabled: %s",
			  mu_strerror (rc));
	  rc = 0; /* Continue anyway */
	  break;
	}
      if (rc)
	return rc;
    }
  
  return 0;
}

static void
smtp_destroy (mu_mailer_t mailer)
{
  struct _smtp_mailer *smp = mailer->data;
  
  mu_address_destroy (&smp->rcpt);
  mu_smtp_destroy (&smp->smtp);
  if (smp->rcpt_hdr)
    mu_argv_free (smp->rcpt_hdr);
  free (smp);

  mailer->data = NULL;
}

static int
smtp_close (mu_mailer_t mailer)
{
  struct _smtp_mailer *smp = mailer->data;
  mu_smtp_t smtp = smp->smtp;  
  int rc = mu_smtp_quit (smtp);
  if (rc == 0)
    {
      mu_smtp_set_carrier (smtp, NULL);
    }
  return rc;
}


static int
smp_change_domain (struct _smtp_mailer *smp, mu_address_t addr)
{
  size_t domlen;

  if (smp->add_domain)
    domlen = strlen (smp->add_domain);
  else if (!smp->strip_domain)
    return 0;

  while (addr)
    {
      char *p;
      if (smp->strip_domain)
	{
	  p = strchr (addr->email, '@');
	  if (p)
	    *p = 0;
	}
      if (smp->add_domain && !strchr (addr->email, '@'))
	{
	  p = realloc (addr->email, strlen (addr->email) + domlen + 2);
	  if (!p)
	    return ENOMEM;
	  strcat (strcat (p, "@"), smp->add_domain);
	  addr->email = p;
	}
      addr = addr->next;
    }
  return 0;
}

int
smtp_address_add (struct _smtp_mailer *smp, mu_address_t *paddr,
		  const char *value)
{
  mu_address_t addr = NULL;
  int rc;

  if ((rc = mu_address_create (&addr, value)) == 0)
    {
      if ((rc = smp_change_domain (smp, addr)) == 0)
	rc = mu_address_union (paddr, addr);
    }
  mu_address_destroy (&addr);
  return rc;
}

static int
_smtp_property_is_set (struct _smtp_mailer *smp, const char *name)
{
  mu_property_t property = NULL;

  mu_mailer_get_property (smp->mailer, &property);
  return mu_property_is_set (property, name);
}


static int
_smtp_set_rcpt (struct _smtp_mailer *smp, mu_message_t msg, mu_address_t to)
{
  int status = 0;

  /* Get RCPT_TO from TO, or the message. */

  if (to)
    {
      /* Use the specified mu_address_t. */
      if ((status = mu_mailer_check_to (to)) != 0)
	{
	  mu_debug (MU_DEBCAT_MAILER, MU_DEBUG_ERROR,
		    ("mu_mailer_send_message(): explicit to not valid"));
	  return status;
	}
      smp->rcpt = mu_address_dup (to);
      if (!smp->rcpt)
        return ENOMEM;

      if (status)
	return status;
    }

  if ((!to || _smtp_property_is_set (smp, "READ_RECIPIENTS")) && smp->rcpt_hdr)
    {
      mu_header_t header;
      const char *value;
      size_t i;

      if ((status = mu_message_get_header (msg, &header)))
	return status;

      for (i = 0; smp->rcpt_hdr[i]; i++)
	{
	  status = mu_header_sget_value (header, smp->rcpt_hdr[i], &value);
	  if (status == 0)
	    smtp_address_add (smp, &smp->rcpt, value);
	  else if (status != MU_ERR_NOENT)
	    break;
	  else
	    status = 0;
	}
      if (status == 0)
	{
	  if (!smp->rcpt)
	    status = MU_ERR_MAILER_NO_RCPT_TO;
	  else
	    status = mu_mailer_check_to (smp->rcpt);
	}
    }

  if (status)
    mu_address_destroy (&smp->rcpt);

  return status;
}

static int
_rcpt_to_addr (mu_smtp_t smtp, mu_address_t addr, size_t *pcount)
{
  size_t i, count, rcpt_cnt = 0;
  int status;
  
  status = mu_address_get_count (addr, &count);
  if (status)
    return status;

  for (i = 1; i <= count; i++)
    {
      const char *to = NULL;

      status = mu_address_sget_email (addr, i, &to);
      if (status == 0 && to)
	{
	  status = mu_smtp_rcpt_basic (smtp, to, NULL);
	  if (status == 0)
	    rcpt_cnt++;
	  else if (status != MU_ERR_REPLY)
	    break;
	}
    }
  *pcount = rcpt_cnt;
  return status;
}

static int
smtp_send_message (mu_mailer_t mailer, mu_message_t msg,
		   mu_address_t argfrom, mu_address_t argto)
{
  struct _smtp_mailer *smp;
  mu_smtp_t smtp;
  int status;
  size_t size, lines, count;
  const char *mail_from, *size_str;
  mu_header_t     header;
      
  if (mailer == NULL)
    return EINVAL;
  
  smp = mailer->data;
  if (!smp)
    return EINVAL;
  smtp = smp->smtp;
  
  if ((status = mu_message_get_header (msg, &header)))
    return status;
  
  status = _smtp_set_rcpt (smp, msg, argto);
  if (status)
    return status;

  status = mu_address_sget_email (argfrom, 1, &mail_from);
  if (status)
    return status;
  if (!mail_from)
    return MU_ERR_NOENT;
      
  if (mu_smtp_capa_test (smtp, "SIZE", &size_str) == 0 &&
      mu_message_size (msg, &size) == 0 &&
      mu_message_lines (msg, &lines) == 0)
    {
      size_t msgsize = size + lines;
      if (strncmp (size_str, "SIZE ", 5) == 0)
	{
	  size_t maxsize = strtoul (size_str + 5, NULL, 10);

	  if (msgsize && maxsize && msgsize > maxsize)
	    return EFBIG;
	}
      status = mu_smtp_mail_basic (smtp, mail_from,
				   "SIZE=%lu",
				   (unsigned long) msgsize);
    }
  else
    status = mu_smtp_mail_basic (smtp, mail_from, NULL);
  if (status)
    {
      if (status == MU_ERR_REPLY)
	mu_smtp_rset (smtp);
      return status;
    }

  status = _rcpt_to_addr (smtp, smp->rcpt, &count);
  if (status && count == 0)
    {
      if (status == MU_ERR_REPLY)
	mu_smtp_rset (smtp);
      return status;
    }

  if (mu_header_sget_value (header, MU_HEADER_BCC, NULL) == 0 ||
      mu_header_sget_value (header, MU_HEADER_FCC, NULL) == 0)
    {
      mu_iterator_t itr;
      mu_body_t body;
      mu_stream_t ostr, bstr;

      status = mu_smtp_data (smtp, &ostr);
      if (status)
	{
	  if (status == MU_ERR_REPLY)
	    mu_smtp_rset (smtp);
	  return status;
	}
      mu_header_get_iterator (header, &itr);
      for (mu_iterator_first (itr); !mu_iterator_is_done (itr);
	   mu_iterator_next (itr))
	{
	  const char *name;
	  void *value;

	  mu_iterator_current_kv (itr, (void*) &name, &value);
	  if (mu_c_strcasecmp (name, MU_HEADER_BCC) == 0 ||
	      mu_c_strcasecmp (name, MU_HEADER_FCC) == 0)
	    continue;
	  mu_stream_printf (ostr, "%s: %s\n", name, (char*)value);
	}
      mu_iterator_destroy (&itr);
      mu_stream_write (ostr, "\n", 1, NULL);
      
      mu_message_get_body (msg, &body);
      mu_body_get_streamref (body, &bstr);
      status = mu_stream_copy (ostr, bstr, 0, NULL);
      mu_stream_destroy (&bstr);
      mu_stream_close (ostr);
      mu_stream_destroy (&ostr);
    }
  else
    {
      mu_stream_t str;
      mu_message_get_streamref (msg, &str);
      status = mu_smtp_send_stream (smtp, str);
      mu_stream_destroy (&str);
    }
  mu_address_destroy (&smp->rcpt);
  if (status == 0)
    {
      status = mu_smtp_dot (smtp);
      if (status == MU_ERR_REPLY)
	mu_smtp_rset (smtp);
    }
  return status;
}

static int
_mailer_smtp_init (mu_mailer_t mailer)
{
  struct _smtp_mailer *smp;
  mu_property_t   property = NULL;

  /* Allocate memory specific to smtp mailer.  */
  smp = mailer->data = calloc (1, sizeof (*smp));
  if (mailer->data == NULL)
    return ENOMEM;

  smp->mailer = mailer;	/* Back pointer.  */
  
  mailer->_destroy = smtp_destroy;
  mailer->_open = smtp_open;
  mailer->_close = smtp_close;
  mailer->_send_message = smtp_send_message;

  /* Set our properties.  */
  mu_mailer_get_property (mailer, &property);
  mu_property_set_value (property, "TYPE", "SMTP", 1);

  return 0;
}

#else
#include <stdio.h>
#include <mailutils/sys/registrar.h>
mu_record_t     mu_smtp_record = NULL;
#endif
