/* GNU Mailutils -- a suite of utilities for electronic mail
   Copyright (C) 2005-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/>. */

/* Syntax: vacation [:days <ndays: number>]
                    [:subject <subject: string>]
		    [:aliases <address-list: list>]
		    [:noreply <noreply-address-list: list>]
		    [:reply_regex <expr: string>]
		    [:reply_prefix <prefix: string>]
		    [:sender <email: string>]
		    [:database <path: string>]
		    [:rfc2822]
		    [:file]
		    [:mime]
		    [:always_reply]
		    [:return_address <email: string>]
		    [:header <header-list: list>]
		    <reply: string>
*/

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

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <signal.h>
#include <regex.h>
#include <mailutils/sieve.h>

/* Build a mime response message from original message MSG. TEXT
   is the message text.
*/
static int
build_mime (mu_sieve_machine_t mach, mu_mime_t *pmime,
	    mu_message_t msg, const char *text)
{
  mu_mime_t mime = NULL;
  mu_message_t newmsg;
  mu_stream_t stream, input;
  mu_header_t hdr;
  mu_body_t body;
  const char *header =
    "Content-Type: text/plain;charset=" MU_SIEVE_CHARSET "\n"
    "Content-Transfer-Encoding: 8bit\n\n";
  int rc;
  
  mu_mime_create (&mime, NULL, 0);
  mu_message_create (&newmsg, NULL);
  mu_message_get_body (newmsg, &body);

  if ((rc = mu_static_memory_stream_create (&input, text, strlen (text))))
    {
      mu_sieve_error (mach,
		      _("cannot create temporary stream: %s"),
		      mu_strerror (rc));
      mu_mime_destroy (&mime);
      mu_message_destroy (&newmsg, NULL);
      return 1;
    }

  if (mu_sieve_get_tag (mach, "mime", SVT_VOID, NULL))
    {
      mu_stream_t fstr;
      rc = mu_filter_create (&fstr, input, "base64",
			     MU_FILTER_ENCODE, 
			     MU_STREAM_READ);
      mu_stream_unref (input); 
      if (rc == 0) 
	{
	  header = "Content-Type: text/plain;charset=" MU_SIEVE_CHARSET "\n"
	           "Content-Transfer-Encoding: base64\n\n";
	  input = fstr;
	}
    }

  rc = mu_body_get_streamref (body, &stream);
  if (rc)
    {
      mu_sieve_error (mach,
		      _("cannot get input body stream: %s"),
		      mu_strerror (rc));
      mu_mime_destroy (&mime);
      mu_message_destroy (&newmsg, NULL);
      mu_stream_destroy (&input);
      return 1;
    }

  rc = mu_stream_copy (stream, input, 0, NULL);
  mu_stream_destroy (&input);
  mu_stream_destroy (&stream);
  
  if (rc)
    {
      mu_sieve_error (mach,
		      _("stream copy failed: %s"),
		      mu_strerror (rc));
      mu_mime_destroy (&mime);
      mu_message_destroy (&newmsg, NULL);
      return 1;
    }

  mu_header_create (&hdr, header, strlen (header));
  mu_message_set_header (newmsg, hdr, NULL);

  mu_mime_add_part (mime, newmsg);
  mu_message_unref (newmsg);

  *pmime = mime;

  return 0;
}



/* Produce diagnostic output. */
static int
diag (mu_sieve_machine_t mach)
{
  mu_sieve_log_action (mach, "VACATION", NULL);
  return mu_sieve_is_dry_run (mach);
}


struct addr_data
{
  mu_address_t addr;
  char *my_address;
};

static int
_compare (void *item, void *data)
{
  struct addr_data *ad = data;
  int rc = mu_address_contains_email (ad->addr, item);
  if (rc)
    ad->my_address = item;
  return rc;
}

/* Check whether an alias from ADDRESSES is part of To: or Cc: headers
   of the originating mail. Return non-zero if so and store a pointer
   to the matching address in *MY_ADDRESS. */
static int
match_addresses (mu_sieve_machine_t mach, mu_header_t hdr, char *email,
		 mu_sieve_value_t *addresses, char const **my_address)
{
  int match = 0;
  const char *str;
  struct addr_data ad;

  ad.my_address = NULL;
  if (mu_header_sget_value (hdr, MU_HEADER_TO, &str) == 0)
    {
      if (!mu_address_create (&ad.addr, str))
	{
	  if (_compare (email, &ad))
	    match = 1;
	  else if (addresses)
	    match += mu_sieve_vlist_do (mach, addresses, _compare, &ad);
	  mu_address_destroy (&ad.addr);
	}
    }

  if (!match && mu_header_sget_value (hdr, MU_HEADER_CC, &str) == 0)
    {
      if (!mu_address_create (&ad.addr, str))
	{
	  if (_compare (email, &ad))
	    match = 1;
	  else if (addresses)
	    match += mu_sieve_vlist_do (mach, addresses, _compare, &ad);
	  mu_address_destroy (&ad.addr);
	}
    }
  *my_address = ad.my_address;
  return match;
}


struct regex_data
{
  mu_sieve_machine_t mach;
  char *email;
};

static int
regex_comparator (void *item, void *data)
{
  regex_t preg;
  int rc;
  struct regex_data *d = data;
  
  if (regcomp (&preg, item,
	       REG_EXTENDED | REG_NOSUB | REG_NEWLINE | REG_ICASE))
    {
      // FIXME: detailed error?
      mu_sieve_error (d->mach,
		      _("%lu: cannot compile regular expression %s"),
		      (unsigned long) mu_sieve_get_message_num (d->mach),
		      mu_quotename ((char*) item));
      return 0;
    }
  rc = regexec (&preg, d->email, 0, NULL, 0) == 0;
  regfree (&preg);
  return rc;
}

/* Decide whether EMAIL address should not be responded to.
 */
static int
noreply_address_p (mu_sieve_machine_t mach, char *email)
{
  int i, rc = 0;
  mu_sieve_value_t *arg;
  struct regex_data rd;
  static char *noreply_sender[] = {
    ".*-REQUEST@.*",
    ".*-RELAY@.*",
    ".*-OWNER@.*",
    "^OWNER-.*",
    "^postmaster@.*",
    "^UUCP@.*",
    "^MAILER@.*",
    "^MAILER-DAEMON@.*",
    NULL
  };

  rd.mach = mach;
  rd.email = email;
  for (i = 0; rc == 0 && noreply_sender[i]; i++)
    rc = regex_comparator (noreply_sender[i], &rd);

  if (!rc && (arg = mu_sieve_get_tag_untyped (mach, "noreply")) != NULL)
    rc = mu_sieve_vlist_do (mach, arg, regex_comparator, &rd);
  
  return rc;
}


/* Return T if letter precedence is 'bulk' or 'junk' */
static int
bulk_precedence_p (mu_header_t hdr)
{
  int rc = 0;
  const char *str;
  if (mu_header_sget_value (hdr, MU_HEADER_PRECEDENCE, &str) == 0)
    {
      rc = mu_c_strcasecmp (str, "bulk") == 0
	   || mu_c_strcasecmp (str, "junk") == 0;
    }
  return rc;
}

#define	DAYS_DEFAULT	7
#define	DAYS_MAX	60

static int
test_and_update_prop (mu_property_t prop, const char *from,
		      time_t now, unsigned int days,
		      mu_sieve_machine_t mach)
{
  const char *result;
  char *timebuf;
  time_t last;
  
  int rc = mu_property_sget_value (prop, from, &result);
  switch (rc)
    {
    case MU_ERR_NOENT:
      break;
      
    case 0:
      if (days == 0)
	return 1;
      last = (time_t) strtoul (result, NULL, 0);
      if (last + (24 * 60 * 60 * days) > now)
	return 1;
      break;
      
    default:
      mu_sieve_error (mach, "%lu: mu_property_sget_value: %s",
		      (unsigned long) mu_sieve_get_message_num (mach),
		      mu_strerror (rc));
      return -1;
    }

  rc = mu_asprintf (&timebuf, "%lu", (unsigned long) now);
  if (rc)
    {
      mu_sieve_error (mach, "%lu: mu_asprintf: %s",
		      (unsigned long) mu_sieve_get_message_num (mach),
		      mu_strerror (rc));
      return -1;
    } 
     
  rc = mu_property_set_value (prop, from, timebuf, 1);
  free (timebuf);
  if (rc)
    {
      mu_sieve_error (mach, "%lu: mu_property_set_value: %s",
		      (unsigned long) mu_sieve_get_message_num (mach),
		      mu_strerror (rc));
      return -1;
    }
  
  rc = mu_property_save (prop);
  if (rc)
    {
      mu_sieve_error (mach, "%lu: mu_property_save: %s",
		      (unsigned long) mu_sieve_get_message_num (mach),
		      mu_strerror (rc));
      return -1;
    }
  return 0;
}

/* Check and update the vacation database. Return 0 if the mail should
   be answered, 0 if it should not, and throw exception if an error
   occurs. */
static int
check_db (mu_sieve_machine_t mach, char *from)
{
  mu_property_t prop;
  char *file;
  unsigned int days;
  int rc;
  mu_stream_t str;
  mu_locker_t locker;
  const char *dbfile = "~/.vacation";
  size_t n;
  
  if (mu_sieve_get_tag (mach, "days", SVT_NUMBER, &n))
    {
      days = n;
      if (days > DAYS_MAX)
	days = DAYS_MAX;
    }
  else
    days = DAYS_DEFAULT;

  mu_sieve_get_tag (mach, "database", SVT_STRING, &dbfile);
  
  file = mu_tilde_expansion (dbfile, MU_HIERARCHY_DELIMITER, NULL);
  if (!file)
    {
      mu_sieve_error (mach, _("%lu: cannot build db file name"),
		      (unsigned long) mu_sieve_get_message_num (mach));
      mu_sieve_abort (mach);
    }

  rc = mu_locker_create_ext (&locker, file, NULL);
  if (rc)
    {
      mu_sieve_error (mach, _("%lu: cannot lock %s: %s"),
		      (unsigned long) mu_sieve_get_message_num (mach),
		      file,
		      mu_strerror (rc));
      free (file);
      mu_sieve_abort (mach);
    }

  rc = mu_file_stream_create (&str, file, MU_STREAM_RDWR|MU_STREAM_CREAT);
  if (rc)
    {
      mu_sieve_error (mach, "%lu: mu_file_stream_create(%s): %s",
		      (unsigned long) mu_sieve_get_message_num (mach),
		      file,
		      mu_strerror (rc));
      mu_locker_destroy (&locker);
      free (file);
      mu_sieve_abort (mach);
    }
  
  free (file);

  rc = mu_property_create_init (&prop, mu_assoc_property_init, str);
  if (rc)
    {
      mu_sieve_error (mach, "%lu: mu_property_create_init: %s",
		      (unsigned long) mu_sieve_get_message_num (mach),
		      mu_strerror (rc));
      mu_locker_destroy (&locker);
      mu_sieve_abort (mach);
    }

  rc = mu_locker_lock (locker);
  if (rc)
    {
      mu_sieve_error (mach, "%lu: cannot lock vacation database: %s",
		      (unsigned long) mu_sieve_get_message_num (mach),
		      mu_strerror (rc));
      mu_property_destroy (&prop);
      mu_sieve_abort (mach);
    }

  rc = test_and_update_prop (prop, from, time (NULL), days, mach);
  mu_property_destroy (&prop);
  mu_locker_unlock (locker);
  mu_locker_destroy (&locker);
  if (rc == -1)
    mu_sieve_abort (mach);

  return rc;
}

/* Add a reply prefix to the subject. *PSUBJECT points to the
   original subject, which must be allocated using malloc. Before
   returning its value is freed and replaced with the new one.
   Default reply prefix is "Re: ", unless overridden by
   "reply_prefix" tag.
 */
static void
re_subject (mu_sieve_machine_t mach, char **psubject)
{
  char *subject;
  char *prefix = "Re";
  
  mu_sieve_get_tag (mach, "reply_prefix", SVT_STRING, &prefix);

  subject = malloc (strlen (*psubject) + strlen (prefix) + 3);
  if (!subject)
    {
      mu_sieve_error (mach,
		      _("%lu: not enough memory"),
		      (unsigned long) mu_sieve_get_message_num (mach));
      return;
    }
  
  strcpy (subject, prefix);
  strcat (subject, ": ");
  strcat (subject, *psubject);
  free (*psubject);
  *psubject = subject;
}

/* Process reply subject header.

   If :subject is set, its value is used.
   Otherwise, if original subject matches reply_regex, it is used verbatim.
   Otherwise, reply_prefix is prepended to it. */

static void
vacation_subject (mu_sieve_machine_t mach,
		  mu_message_t msg, mu_header_t newhdr)
{
  char *value;
  char *subject;
  int subject_allocated = 0;
  mu_header_t hdr;
  
  if (mu_sieve_get_tag (mach, "subject", SVT_STRING, &subject))
    /* nothing */;
  else if (mu_message_get_header (msg, &hdr) == 0
	   && mu_header_aget_value_unfold (hdr, MU_HEADER_SUBJECT,
					   &value) == 0)
    {
      char *p;

      int rc = mu_rfc2047_decode (MU_SIEVE_CHARSET, value, &p);

      subject_allocated = 1;
      if (rc)
	{
	  subject = value;
	  value = NULL;
	}
      else
	{
	  subject = p;
	}

      if (mu_sieve_get_tag (mach, "reply_regex", SVT_STRING, &p))
	{
	  char *err = NULL;
	  
	  rc = mu_unre_set_regex (p, 0, &err);
	  if (rc)
	    {
	      mu_sieve_error (mach,
			      _("%lu: cannot compile reply prefix regexp: %s: %s"),
			      (unsigned long) mu_sieve_get_message_num (mach),
			      mu_strerror (rc),
			      mu_prstr (err));
	    }
	}
	  
      if (mu_unre_subject (subject, NULL))
	re_subject (mach, &subject);
      
      free (value);
    }
  else
    subject = "Re: Your mail";
    
  if (mu_rfc2047_encode (MU_SIEVE_CHARSET, "quoted-printable",
			 subject, &value))
    mu_header_set_value (newhdr, MU_HEADER_SUBJECT, subject, 1);
  else
    {
      mu_header_set_value (newhdr, MU_HEADER_SUBJECT, value, 1);
      free (value);
    }

  if (subject_allocated)
    free (subject);
}

static int
header_split (const char *str, char **hname, char **hval)
{
  char *p, *q, *fn, *fv;
  size_t n;
  
  q = strchr (str, ':');
  if (!q)
    return MU_ERR_FORMAT;

  for (p = q; p > str && mu_isspace (p[-1]); --p)
    ;
  if (p == str)
    return MU_ERR_FORMAT;
  
  n = p - str;
  fn = malloc (n + 1);
  if (!fn)
    return ENOMEM;

  memcpy (fn, str, n);
  fn[n] = 0;

  for (++q; *q && mu_isspace (*q); ++q)
    ;

  fv = strdup (q);
  if (!fv)
    {
      free (fn);
      return ENOMEM;
    }

  *hname = fn;
  *hval = fv;

  return 0;
}

struct header_closure
{
  mu_sieve_machine_t mach;  
  mu_header_t hdr;
};

static int
add_header (void *item, void *data)
{
  char const *str = item;
  struct header_closure *hc = data;
  char *fn, *fv;
  int rc;

  rc = header_split (str, &fn, &fv);
  if (rc)
    {
      mu_sieve_error (hc->mach,
		      _("%lu: cannot add header %s: %s"),
		      (unsigned long) mu_sieve_get_message_num (hc->mach),
		      mu_quotename (str), mu_strerror (rc));
      return 0;
    }

  rc = mu_header_append (hc->hdr, fn, fv);
  free (fn);
  free (fv);
  
  if (rc)
    mu_sieve_error (hc->mach,
		    _("%lu: cannot add header %s: %s"),
		    (unsigned long) mu_sieve_get_message_num (hc->mach),
		    mu_quotename (str), mu_strerror (rc));
  return 0;
}

/* Generate and send the reply message */
static int
vacation_reply (mu_sieve_machine_t mach, mu_message_t msg,
		char const *text, char const *to, char const *from)
{
  mu_mime_t mime = NULL;
  mu_message_t newmsg;
  mu_header_t newhdr;
  mu_address_t to_addr = NULL, from_addr = NULL;
  char *value;
  mu_mailer_t mailer;
  int rc;
  mu_sieve_value_t *val;

  if (mu_sieve_get_tag (mach, "file", SVT_VOID, NULL))
    {
      mu_stream_t instr;
      
      rc = mu_mapfile_stream_create (&instr, text, MU_STREAM_READ);
      if (rc)
	{
	  mu_sieve_error (mach,
			  _("%lu: cannot open message file %s: %s"),
			  (unsigned long) mu_sieve_get_message_num (mach),
			  text,
			  mu_strerror (rc));
	  return -1;
	}
      
      if (mu_sieve_get_tag (mach, "rfc2822", SVT_VOID, NULL))
	{
	  rc = mu_stream_to_message (instr, &newmsg);
	  mu_stream_unref (instr);
	  if (rc)
	    {
	      mu_sieve_error (mach,
			      _("%lu: cannot read message from file %s: %s"),
			      (unsigned long) mu_sieve_get_message_num (mach),
			      text,
			      mu_strerror (rc));
	      return -1;
	    }
	}
      else
	{
	  mu_stream_t text_stream;
	  mu_transport_t trans[2];

	  rc = mu_memory_stream_create (&text_stream, MU_STREAM_RDWR);
	  if (rc)
	    {
	      mu_stream_unref (instr);
	      mu_sieve_error (mach,
			      _("%lu: cannot create memory stream: %s"),
			      (unsigned long) mu_sieve_get_message_num (mach),
			      mu_strerror (rc));
	      return -1;
	    }

	  rc = mu_stream_copy (text_stream, instr, 0, NULL);
	  mu_stream_unref (instr);
	  if (rc == 0)
	    rc = mu_stream_write (text_stream, "", 1, NULL);
	  if (rc)
	    {
	      mu_sieve_error (mach,
			      _("%lu: failed reading from %s: %s"),
			      (unsigned long) mu_sieve_get_message_num (mach),
			      text,
			      mu_strerror (rc));
	      return -1;
	    }

	  rc = mu_stream_ioctl (text_stream, MU_IOCTL_TRANSPORT,
				MU_IOCTL_OP_GET, trans);
	  if (rc)
	    {
	      mu_stream_unref (text_stream);
	      mu_sieve_error (mach,
			      "%lu: mu_stream_ioctl: %s",
			      (unsigned long) mu_sieve_get_message_num (mach),
			      mu_strerror (rc));
	      return -1;
	    }

	  if (build_mime (mach, &mime, msg, (char const *) trans[0]))
	    {
	      mu_stream_unref (text_stream);
	      return -1;
	    }
	  mu_mime_get_message (mime, &newmsg);
	  mu_message_unref (newmsg);
	  mu_stream_unref (text_stream);
	}
    }
  else
    {
      if (build_mime (mach, &mime, msg, text))
	return -1;
      mu_mime_get_message (mime, &newmsg);
      mu_message_unref (newmsg);
    }
  
  mu_message_get_header (newmsg, &newhdr);
  
  rc = mu_address_create (&to_addr, to);
  if (rc)
    {
      mu_sieve_error (mach,
		      _("%lu: cannot create recipient address <%s>: %s"),
		      (unsigned long) mu_sieve_get_message_num (mach),
		      from, mu_strerror (rc));
    }
  else
    {
      mu_header_set_value (newhdr, MU_HEADER_TO, to, 1);

      val = mu_sieve_get_tag_untyped (mach, "header");
      if (val)
	{
	  struct header_closure hc;
	  hc.mach = mach;
	  hc.hdr = newhdr;
	  mu_sieve_vlist_do (mach, val, add_header, &hc);
	}
      
      vacation_subject (mach, msg, newhdr);
      
      if (from)
        {
          if (mu_address_create (&from_addr, from))
	    from_addr = NULL;
        }
      else
        {
          from_addr = NULL;
        }
      
      if (mu_rfc2822_in_reply_to (msg, &value) == 0)
        {
          mu_header_set_value (newhdr, MU_HEADER_IN_REPLY_TO, value, 1);
          free (value);
        }
      
      if (mu_rfc2822_references (msg, &value) == 0)
        {
          mu_header_set_value (newhdr, MU_HEADER_REFERENCES, value, 1);
          free (value);
        }
      
      mailer = mu_sieve_get_mailer (mach);
      if (mailer)
	{
	  rc = mu_mailer_send_message (mailer, newmsg, from_addr, to_addr);
	}
      else
	rc = MU_ERR_FAILURE;
    }
  mu_address_destroy (&to_addr);
  mu_address_destroy (&from_addr);
  mu_mime_destroy (&mime);
  return rc;
}

int
sieve_action_vacation (mu_sieve_machine_t mach)
{
  int rc;
  char *text, *from = NULL;
  char const *return_address;
  mu_message_t msg;
  mu_header_t hdr;
  char *my_address;
  
  if (diag (mach))
    return 0;
  
  mu_sieve_get_arg (mach, 0, SVT_STRING, &text);

  msg = mu_sieve_get_message (mach);
  mu_message_get_header (msg, &hdr);

  if (mu_sieve_get_tag (mach, "sender", SVT_STRING, &from))
    {
      /* Debugging hook: :sender sets fake reply address */
      from = strdup (from);
      if (!from)
        {
          mu_sieve_error (mach, "%lu: %s",
                          (unsigned long) mu_sieve_get_message_num (mach),
                          mu_strerror (ENOMEM));
          mu_sieve_abort (mach);
        }
    }
  else if ((rc = mu_sieve_get_message_sender (msg, &from)) != 0)
    {
      mu_sieve_error (mach,
		      _("%lu: cannot get sender address: %s"),
		      (unsigned long) mu_sieve_get_message_num (mach),
		      mu_strerror (rc));
      mu_sieve_abort (mach);
    }

  my_address = mu_get_user_email (NULL);

  if (mu_sieve_get_tag (mach, "always_reply", SVT_VOID, NULL))
    return_address = my_address;
  else
    {
      mu_sieve_value_t *val = mu_sieve_get_tag_untyped (mach, "aliases");
      if (match_addresses (mach, hdr, my_address, val, &return_address) == 0)
	{
	  free (my_address);
	  return 0;
	}
    }

  if (noreply_address_p (mach, from)
      || bulk_precedence_p (hdr)
      || check_db (mach, from))
    {
      free (from);
      free (my_address);
      return 0;
    }

  mu_sieve_get_tag (mach, "return_address", SVT_STRING,
		       &return_address);

  rc = vacation_reply (mach, msg, text, from, return_address);
  free (from);
  free (my_address);
  if (rc == -1)
    mu_sieve_abort (mach);
  return rc;
}

/* Tagged arguments: */
static mu_sieve_tag_def_t vacation_tags[] = {
  {"days", SVT_NUMBER},
  {"subject", SVT_STRING},
  {"aliases", SVT_STRING_LIST},
  {"noreply", SVT_STRING_LIST},
  {"reply_regex", SVT_STRING},
  {"reply_prefix", SVT_STRING},
  {"sender", SVT_STRING},
  {"database", SVT_STRING},
  {"mime", SVT_VOID},
  {"file", SVT_VOID},
  {"always_reply", SVT_VOID},
  {"return_address", SVT_STRING},
  {"header", SVT_STRING_LIST},
  {"rfc2822", SVT_VOID},
  {NULL}
};

static mu_sieve_tag_group_t vacation_tag_groups[] = {
  {vacation_tags, NULL},
  {NULL}
};

/* Required arguments: */
static mu_sieve_data_type vacation_args[] = {
  SVT_STRING,			/* message text */
  SVT_VOID
};

int SIEVE_EXPORT (vacation, init) (mu_sieve_machine_t mach)
{
  mu_sieve_register_action (mach, "vacation", sieve_action_vacation,
			    vacation_args, vacation_tag_groups, 1);
  return 0;
}
