/*
 *  virus_scanner_simple.c
 *
 *    Remove potentially harmful content from queued messages.
 *
 *  Copyright (c) 2003, Sun Microsystems, Inc.  All Rights Reserved.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include "mtasdk.h"

/*
 *  A structure to store our channel options
 */
typedef struct {
     int     debug;                          /* Produce debug output?       */
     char    bad_mime_types[BIGALFA_SIZE+3]; /* Unwanted MIME content types */
     size_t  bmt_len;                        /* Length of bmt string        */
     char    bad_file_types[BIGALFA_SIZE+3]; /* Unwanted file types         */
     size_t  bft_len;                        /* Length of bft string        */
} our_options_t;


/*
 *  Forward declarations
 */
static void error_exit(int ires, const char *msg);
static void error_report(our_options_t *options, int ires, const char *func);
static int  is_bad_mime_type(our_options_t *options, mta_decode_t *dctx,
  char *buf, size_t maxbuflen);
static int is_bad_file_type(our_options_t *options, mta_opt_t *params,
  const char *param_name, char *buf, size_t maxbuflen);
static int  load_options(our_options_t *options);

static mta_dq_process_message_t process_message;
static mta_decode_read_t decode_read;
static mta_decode_inspect_t decode_inspect;


/*
 *  main() -- Initialize the MTA SDK, load our options, and then start the
 *            message processing loop.
 */
int main()
{
     int ires;
     our_options_t options;

     /*
      *  Initialize the MTA SDK
      */
     if ((ires = mtaInit(0)))
	  error_exit(ires, "Unable to initialize the MTA SDK");

     /*
      *  Load our channel options
      */
     if ((ires = load_options(&options)))
	  error_exit(ires, "Unable to load our channel options");
	 
     /*
      *  Now process the queued messages.  Be sure to indicate a thread
      *  stack size sufficient to accomodate message enqueue processing.
      */
     if ((ires = mtaDequeueStart((void *)&options, process_message, NULL,
				 MTA_THREAD_STACK_SIZE, mtaStackSize(), 0)))
	  error_exit(ires, "Error during dequeue processing");

     /*
      *  All done
      */
     mtaDone();
     return(0);
}


/*
 *  process_message() -- This routine is called by mtaDequeueStart() to process
 *                       each queued message.  We don't make use of ctx2, but
 *                       ctx1 is a pointer to our channel options.
 */
static int process_message(void **ctx2, void *ctx1, mta_dq_t *dq,
			   const char *env_from, size_t env_from_len)
{
     const char *adr;
     int disp, ires;
     size_t len;
     mta_nq_t *nq;
     our_options_t *options = (our_options_t *)ctx1;

     /*
      *  Initializations
      */
     nq = NULL;

/*
 *  A little macro to do error checking on mta*() calls
 */
#define CHECK(f,x) \
     if ((ires = x)) { error_report(options, ires, f); goto done_bad; }

     /*
      *  Start a message enqueue.  Use the dequeue context to copy
      *  envelope flags fromt the current message to this new message
      *  being enqueued.
      */
     CHECK("mtaEnqueueStart",
	   mtaEnqueueStart(&nq, env_from, env_from_len, MTA_DQ_CONTEXT, dq, 0));

     /*
      *  Process the envelope recipient list
      */
     while (!(ires = mtaDequeueRecipientNext(dq, &adr, &len, 0)))
     {
	  /*
	   *  Add this envelope recipient address to the message being
	   *  enqueued.  Use the dequeue context to copy envelope flags
	   *  for this recipient from the current message to this new
	   *  message.
	   */
	  ires = mtaEnqueueTo(nq, adr, len, MTA_DQ_CONTEXT, dq,
			      MTA_ENV_TO, 0);
	  disp = (ires) ? MTA_DISP_DEFERRED : MTA_DISP_RELAYED;
	  CHECK("mtaDequeueRecipientDisposition",
		mtaDequeueRecipientDisposition(dq, adr, len, disp, 0));
     }

     /*
      *  A normal exit from the loop occurs when mtaDequeueRecipientNext()
      *  returns an MTA_EOF status.  Any other status signifies an error.
      */
     if (ires != MTA_EOF)
     {
	  error_report(options, ires, "mtaDequeueRecipientNext");
	  goto done_bad;
     }

     /*
      *  Begin the MIME decode of the message
      */
     CHECK("mtaDecodeMessage",
	   mtaDecodeMessage(
	      (void *)options,            /* Private context is our options   */
	      MTA_DECODE_DQ, (void *)dq,  /* Input is the msg being dequeued  */
	      MTA_DECODE_NQ, (void *)nq,  /* Output is the msg being enqueued */
	      decode_inspect,             /* Inspection routine               */
	      MTA_DECODE_THURMAN,         /* Convert non-MIME formats to MIME */
	      0));

     /*
      *  Finish the enqueue
      *  NOTE: IT'S IMPORTANT TO DO THIS before DOING THE DEQUEUE.
      *        YOU WILL LOSE MAIL IF YOU DO THE DEQUEUE FIRST and
      *        then THE ENQUEUE FAILS. 
      */
     CHECK("mtaEnqueueFinish", mtaEnqueueFinish(nq, 0));
     nq = NULL;

     /*
      *  Finish the dequeue
      */
     CHECK("mtaDequeueMessageFinish", mtaDequeueMessageFinish(dq, 0));

     /*
      *  All done with this message
      */
      return(MTA_OK);

done_bad:
     /*
      *  Abort any ongoing enqueue or dequeue
      */
     if (nq)
	  mtaEnqueueFinish(nq, MTA_ABORT, 0);
     if (dq)
	  mtaDequeueMessageFinish(dq, MTA_ABORT, 0);

     /*
      *  And return our error status
      */
     return(ires);
}

#undef CHECK


/*
 *  decode_inspect() -- This is the routine which inspects each message
 *                      part, deciding whether to accept or reject it.
 */
static int decode_inspect(void *ctx, mta_decode_t *dctx, int data_type,
			  const char *data, size_t data_len)
{
     char buf[BIGALFA_SIZE * 2 + 10];
     int i;
     our_options_t *options = (our_options_t *)ctx;

     /*
      *  See if the part has
      *
      *    1. A bad MIME content-type,
      *    2. A bad file name extension in the (deprecated) NAME= content-type
      *       parameter, or
      *    3. A bad file name extension in the FILENAME= content-disposition
      *       parameter.
      */
     i = 0;
     if ((i = is_bad_mime_type(ctx, dctx, buf, sizeof(buf))) ||
	 is_bad_file_type(ctx,
			  mtaDecodeMessageInfoParams(dctx,
						     MTA_DECODE_CTYPE_PARAMS,
						     NULL),
			  "NAME", buf, sizeof(buf)) ||
	 is_bad_file_type(ctx,
			  mtaDecodeMessageInfoParams(dctx,
						     MTA_DECODE_CDISP_PARAMS,
						     NULL),
			  "FILENAME", buf, sizeof(buf)))
     {
	  char msg[BIGALFA_SIZE*4 + 10];

	  /*
	   *  Replace this part with a text message indicating that the
	   *  part's content has been deleted.
	   */
	  if (i)
	       i = sprintf(msg,
"The content of this message part has been removed.\n"
"It contained a potentially harmful media type of %.*s",
			   strlen(buf)-2, buf+1);
	  else
	       i = sprintf(msg,
"The content of this message part has been removed.\n"
"It contained a potentially harmful file named \"%s\"", buf);
	  return(mtaDecodeMessagePartDelete(dctx, MTA_REASON, msg, i,
					    MTA_DECODE_CTYPE, "text", 4,
					    MTA_DECODE_CSUBTYPE, "plain", 5,
					    MTA_DECODE_CCHARSET, "us-ascii", 8,
					    MTA_DECODE_CDISP, "inline", 6,
					    MTA_DECODE_CLANG, "en", 2, 0));
     }
     else
	  /*
	   *  Keep the part
	   */
	  return(mtaDecodeMessagePartCopy(dctx, 0));
}


/*
 *  is_bad_mime_type() -- See if the part's media type is in our list of
 *                        bad MIME content types (e.g., application/vbscript).
 */
static int is_bad_mime_type(our_options_t *options, mta_decode_t *dctx,
			    char *buf, size_t maxbuflen)
{
     const char *csubtype, *ctype;
     size_t i, len1, len2;
     char *ptr;

     /*
      *  Sanity checks
      */
     if (!options || !options->bmt_len || !options->bad_mime_types[0] || !dctx)
	  return(0);

     /*
      *  Get the MIME content type
      */
     ctype = mtaDecodeMessageInfoString(dctx, MTA_DECODE_CTYPE, NULL, &len1);
     csubtype = mtaDecodeMessageInfoString(dctx, MTA_DECODE_CSUBTYPE, NULL, &len2);

     /*
      *  Build the string <0x01>type/subtype<0x01><0x00>
      */
     ptr = buf;
     *ptr++ = (char)0x01;
     for (i = 0; i < len1; i++)
	  *ptr++ = tolower(*ctype++);
     *ptr++ = '/';
     for (i = 0; i < len2; i++)
	  *ptr++ = tolower(*csubtype++);
     *ptr++ = (char)0x01;
     *ptr = '\0';

     /*
      *  Now see if <0x01>type/subtype<0x01> occurs in the
      *  list of bad MIME content types
      */
     return((strstr(options->bad_mime_types, buf)) ? -1 : 0);
}


/*
 *  is_bad_file_type() -- See if the part has an associated file name whose
 *                        file extension is in our list of bad file extensions
 *                        (e.g., .vbs).
 */
static int is_bad_file_type(our_options_t *options, mta_opt_t *params,
			    const char *param_name, char *buf, size_t maxbuflen)
{
     const char *ptr1;
     char fext[BIGALFA_SIZE+2], *ptr2;
     size_t i, len;

     /*
      *  Sanity checks
      */
     if (!options || !options->bft_len || !params || !param_name)
	  return(0);

     len = 0;
     buf[0] = '\0';
     if (mtaOptionString(params, param_name, 0, buf, &len, maxbuflen - 1) ||
	 !len || !buf[0])
	  /*
	   *  No file name parameter specified
	   */
	  return(0);

     /*
      *  A file name parameter was specified.  Parse it to extract the
      *  file extension portion, if any.
      */
     ptr1 = strrchr(buf, '.');
     if (!ptr1)
	  /*
	   *  No file extension specified
	   */
	  return(0);

     /*
      *  Now store in fext[] the string <0x01>extension<0x01><0x00>.
      *  Note that we drop the '.' from the extension.
      */
     ptr1++; /* Skip over the '.' */
     ptr2 = fext;
     *ptr2++ = (char)0x01;
     len = len - (ptr1 - buf);
     for (i = 0; i < len; i++)
	  *ptr2++ = tolower(*ptr1++);
     *ptr2++ = (char)0x01;
     *ptr2++ = '\0';

     /*
      *  Now return -1 if the string <0x01>extension<0x01> occurs
      *  in options->bad_file_types.
      */
     return((strstr(options->bad_file_types, fext)) ? -1 : 0);
}


/*
 *  load_options() -- Load our channel options from the channel's option file.
 */
static int load_options(our_options_t *options)
{
     char buf[BIGALFA_SIZE+1];
     size_t buflen, i;
     mta_opt_t *channel_opts;
     int ires;
     const char *ptr0;
     char *ptr1;

     /*
      *  Initialize the our private channel option structure
      */
     memset(options, 0, sizeof(our_options_t));

     /*
      *  Access the channel's option file
      */
     channel_opts = NULL;
     if ((ires = mtaOptionStart(&channel_opts, NULL, 0, 0)))
     {
	  mtaLog("Unable to access our channel option file");
	  return(ires);
     }

     /*
      *  DEBUG=0|1
      */
     options->debug = 0;
     mtaOptionInt(channel_opts, "DEBUG", 0, &options->debug);
     if (options->debug)
	  mtaDebug(MTA_DEBUG_SDK, 0);

     /*
      *  BAD_MIME_TYPES=type1/subtype1[,type2/subtype2[,...]]
      */
     buf[0] = '\0';
     buflen = 0;
     mtaOptionString(channel_opts, "BAD_MIME_TYPES", 0, buf, &buflen,
		     sizeof(buf));

     /*
      *  Now translate the comma separated list
      *
      *    Type1/Subtype1[,Type2/Subtype2[,...]]
      *
      *  to
      *
      *    <0x01>type1/subtype1[<0x01>type2/subtype2[<0x01>...]]<0x01>
      */
     ptr0 = buf;
     ptr1 = options->bad_mime_types;
     *ptr1++ = (char)0x01;
     for (i = 0; i < buflen; i++)
     {
	  if (*ptr0 != ',')
	       *ptr1++ = tolower(*ptr0++);
	  else
	  {
	       *ptr1++ = (char)0x01;
	       ptr0++;
	  }
     }
     *ptr1++ = (char)0x01;
     *ptr1   = '\0';
     options->bmt_len = ptr1 - options->bad_mime_types;

     /*
      *  BAD_FILE_TYPES=["."]Ext1[,["."]Ext2[,...]]
      */
     buf[0] = '\0';
     buflen = 0;
     mtaOptionString(channel_opts, "BAD_FILE_TYPES", 0, buf, &buflen,
		     sizeof(buf));

     /*
      *  Now translate the comma separated list
      *
      *    ["."]Ext1[,["."]Ext2[,...]]
      *
      *  to
      *
      *    <0x01>ext1[<0x01>ext2[<0x01>...]]<0x01>
      */
     ptr0 = buf;
     ptr1 = options->bad_file_types;
     *ptr1++ = (char)0x01;
     for (i = 0; i < buflen; i++)
     {
	  switch(*ptr0)
	  {
	  default :   /* copy after translating to lower case */
	       *ptr1++ = tolower(*ptr0++);
	       break;

	  case '.' :  /* discard */
	       break;

	  case ',' :  /* end current type */
	       *ptr1++ = (char)0x01;
	       ptr0++;
	       break;
	  }
     }
     *ptr1++ = (char)0x01;
     *ptr1   = '\0';
     options->bft_len = ptr1 - options->bad_file_types;

     /*
      *  Dispose of the mta_opt_t context
      */
     mtaOptionFinish(channel_opts);

     /*
      *  And return a success
      */
     return(MTA_OK);
}


/*
 *  error_report() -- Report an error condition when debugging is enabled.
 */
static void error_report(our_options_t *options, int ires, const char *func)
{
     if (options->debug)
	  mtaLog("%s() returned %d; %s",
		 (func ? func : "?"), ires, mtaStrError(ires, 0));
}


/*
 *  error_exit() -- Exit with an error status and error message.
 */
static void error_exit(int ires, const char *msg)
{
     mtaLog("%s%s%s",
	    (msg ? msg : ""), (msg ? "; " : ""), mtaStrError(ires, 0));
     exit(1);
}
