/*
 * CGI script for sending mail, driven by a template file that inserts
 * form fields at specified spots.
 *
 * Usage:
 *   Create HTML form that specifies this script as the action with the
 *   template file as the path_info following the script name.
 *
 * Template file format:
 *    Template file consists of a header followed by the body section, the
 *    end of header is indicated by a blank line.  At any point in the
 *    file you can specify a form field name surrounded by square brackets
 *    and the corresponding field from the form will be substituted.
 *
 * Template header fields:
 *    To: address
 *    Subject: string
 *    Success: url
 *    
 *    Fail: url
 * 
 *  AUTHORS:
 *
 *      Dick Munroe			Original CGIMAILTO program.
 *	Acorn Software, Inc.
 *	munroe@acornsw.com
 *
 *      David Jones
 *	Ohio State University
 *
 *  CREATION DATE:  04-Dec-94
 * Date: 13-JUN-1996
 * Revised: 16-JUN-1996
 * Revised:  30-MAY-1997
 */
#if defined(__ALPHA)
/* Some structures needed by mail are not aligned */
#pragma nomember_alignment
#endif

/** MGD **/
#if defined(__ia64)
#pragma nomember_alignment
#endif

#include <mail$routines.h>
#include <stdio.h>
#include <stat.h>

#include "cgilib.h"
struct ItemList { short int length, code; char *buffer; long returnLength; };

#include <descrip.h>
#include <jpidef.h>
#include <lib$routines.h>
#include <maildef.h>
#include <ssdef.h>
#include <stdlib.h>
#include <str$routines.h>
#include <string.h>
#include <ctype.h>
#include <stsdef.h>
/*
 * Miscellaneous data structures used to store form data and text lines.
 */
struct field_def { struct field_def *next; char *key, *value;
	int klen; };
typedef struct field_def *field_def_p;
struct scanctx {
    char *body;
    char *subline;
};
struct outrec {
    struct outrec *next;
    int length;
    char data[1];	/* variable length actually allocated */
};
static int ent_flag = 0;	/* if true, entify chars "<>&" in expansions */
/*
 * Forward function references.
 */
static int scan_body ( struct scanctx *context, field_def_p fields,
	char *buffer, int bufsize, int *length );
static char *expand_line ( char *, field_def_p );
static char *reformat_value ( char *source, char *format, int fmt_len );
static char *entify_string ( char *source );
static field_def_p form_data();
static void send_fail ( char *stsline, char *reason, void *arg )
{
    char fmt[256];
    sprintf(fmt,"context-type: text/plain\nstatus: %s\n\nSend failed: %s\n", 
		stsline, reason );
    cgi_printf(fmt,arg,arg);
    exit(1);
}
/****************************************************************************/
/* Main routine.
 */
int main ( int argc, char **argv )
{
    char buffer[1024], body_line[256];
    char *template_name, *template_file, *template;
    char *method, *personal_name, *t_body, *expanded, *cp ;
    struct scanctx context;
    struct ItemList itemList[10], nullItemList = {0, 0, 0, 0} ;
    unsigned long sendContext = 0 ;
    char *to, *subj, *version, *success, *failure, *line;
    FILE *tfile;
    field_def_p field_list, fld;
    struct outrec rec_list, *last_rec, *cur_rec;
    int i, j, status, length, t_used, t_alloc, succ_sts;
    /*
     * Initialize CGI and load content into memory.
      */
    status = cgi_init ( argc, argv );
    if ( (status&1) == 0 ) return status;
    field_list = form_data();
    if ( !field_list ) return 1;
    /*
     * Get template file name and attempt to open.
     */
    template_name = cgi_info ( "PATH_INFO" );
    template_file = cgi_info ( "PATH_TRANSLATED" );
    if ( !template_name || !template_file ) 
	send_fail ( "500 bad template", "Bad template name (%s)\n",
		template_name ? template_name : "???" );

    tfile = fopen ( template_file, "r" );
    if ( !tfile ) send_fail ( "500 open failure",
	"open failure on template file (%s)", template_file );
    /*
     * Read template data into memory.
     */
    t_alloc = 8192;
    template = malloc(t_alloc);
    t_used = 0;
    while (0 < (length=fread(&template[t_used], 1, t_alloc - t_used, tfile))) {
	t_used += length;
	if ( t_used >= t_alloc ) {
	    t_alloc = t_alloc + 20000;
	    if ( t_alloc > 100000 ) send_fail ( "500 error",
		"template file too big (%s)", template_name );
	    template = realloc ( template, t_alloc );
	}
    }
    fclose ( tfile );
    /*
     * Parse header out of template, check that first line is version.
     */
    t_body = version = (char *) 0;
    for ( i = 0; i < t_used; i++ ) if ( template[i] == '\n' ) {
	if ( !version ) {
	    /* First line must be "tmail" */
	    for ( j = 0; template[j] && template[j] != ':'; j++ ) 
			template[j] = tolower(template[j]);
	    if ( 0 != strncmp(template,"tmail:", 6) ) send_fail (
		"500 invalid template",
		"first line of template must be tmail: header", 0 );
	    version = &template[7];
	}
	if ( template[i+1] == '\n' ) {
	    template[i+1] = '\0';
	    t_body = &template[i+2];
	    break;
	}
    }
    if ( !t_body ) send_fail ( "500 invalid template", 
	"Invalid template format - no body", 0 );
    /*
     * Parse header lines.
     */
    to = subj = success = failure = (char *) 0;
    succ_sts = 0;
    for ( i = 0, line = template; template[i]; i++ ) {
	if (template[i] == '\n') {
	    template[i] = '\0';
	    for ( j = 0; line[j] && line[j] != ':'; j++ ) 
			line[j] = tolower(line[j]);
	    if ( strncmp(line,"to:",3)==0 ) 
		to = expand_line(&line[3],field_list);
	    else if ( strncmp(line,"subject:",8) == 0 ) 
		subj = expand_line(&line[8], field_list);
	    else if ( strncmp(line,"success:",8) == 0 )
		success = expand_line ( &line[8], field_list );
	    else if ( strncmp(line,"success-status:",15) == 0 )
		succ_sts = atoi(&line[15]);
	    else if ( strncmp(line,"failure:",8) == 0 )
		failure = expand_line ( &line[8], field_list );
	    line = &template[i+1];
	}
    }
    if ( !to ) send_fail ( "500 invalid template",
	"invalid template file header - no to: line", 0) ;
    /*
     * Parse the body lines and generate list of records.
     */
    context.body = t_body;
    context.subline = (char *) 0;

    for ( last_rec = &rec_list; ; last_rec  = cur_rec ) {
	status = scan_body (&context, field_list, body_line, 
		sizeof(body_line)-1, &length );
	if ( status > 0 ) {
	    /*
	     * Add record to list.
	     */
	    cur_rec = (struct outrec *) malloc (sizeof(struct outrec)+length);
	    last_rec->next = cur_rec;
	    if ( !cur_rec ) return 0;
	    cur_rec->next = (struct outrec *) 0;
	    cur_rec->length = length;
	    memcpy ( cur_rec->data, body_line, length );
	    cur_rec->data[length] = '\0';	/* not really necessary */
	} else if ( status == 0 ) {
	    break;	/* status == 0 ==> EOD */
	} else {
	    /*  Error */
	    return 1;
	}
    }
    /*
     * Initiate mail message, construct a personal name for the
     * message from the path used to invoke it.
     */
    personal_name = malloc ( strlen(template_name) + 20 );
    sprintf ( personal_name, "tmail: %s", template_name );
    i = -1;
    itemList[++i].length =		strlen(personal_name);
    itemList[i].code =			MAIL$_SEND_PERS_NAME ;
    itemList[i].buffer =		personal_name;
    itemList[i].returnLength =	0 ;
    itemList[++i].length =		0 ;
    itemList[i].code =			0 ;

    status =
	mail$send_begin (
	    &sendContext,
	    &itemList,
	    &nullItemList) ;		/* Set up to construct the mail context.				 */
    if (!$VMS_STATUS_SUCCESS(status)) send_fail ( "500 mail error",
		"MAIL$SEND_BEGIN returned %%X%0X",(void *) status) ;

    /*
    ** Now build up the attributes of the message.  
    */
    i = -1 ;
    while ( isspace(*to) && *to ) to++;		/* trim leading spaced */
    itemList[++i].length =		strlen(to) ;
    itemList[i].code =			MAIL$_SEND_TO_LINE ;
    itemList[i].buffer =		to ;
    itemList[i].returnLength =	0 ;

    if ( !subj ) subj = "Mail from tmail script";
    while ( isspace(*subj) && *subj ) subj++;	/* trim leading spaced */
    itemList[++i].length =		strlen(subj);
    itemList[i].code =			MAIL$_SEND_SUBJECT ;
    itemList[i].buffer =		subj;
    itemList[i].returnLength =	0 ;

    itemList[++i].length =		0 ;
    itemList[i].code =			0 ;

    status =
	mail$send_add_attribute (
	    &sendContext,
	    &itemList,
	    &nullItemList) ;				/* Add the the message header.						 */
    if (!$VMS_STATUS_SUCCESS(status)) send_fail ( "500 mail error",
	"MAIL$SEND_ADD_ATTRIBUTE returned %%X%0X,\ncheck template's subject: line", 
	(void *) status );
    /*
     * Set destination for to: header line.
     */
    i = -1 ;
    itemList[++i].length = strlen(to) ;
    itemList[i].code =	MAIL$_SEND_USERNAME ;
    itemList[i].buffer =		to ;
    itemList[i].returnLength =	0 ;
    itemList[++i] = nullItemList ;			/* The itemlist describes an address of a receipient.			 */

    status =
	mail$send_add_address (
	    &sendContext,
	    &itemList,
	    &nullItemList) ;				/* Add the sender of the message to the list of receipients.		 */
    if (!$VMS_STATUS_SUCCESS(status)) send_fail ( "500 mail error", 
	"MAIL$SEND_ADD_ADDRESS returned %%X%0X,\ncheck template's to: header line", 
	(void *) status);
    /*
     * Send the items.
     */
    itemList[0].code = MAIL$_SEND_RECORD;
    itemList[0].returnLength = 0;
    itemList[1] = nullItemList;
    for ( cur_rec = rec_list.next; cur_rec; cur_rec = cur_rec->next ) {
	itemList[0].length = cur_rec->length;
        itemList[0].buffer = cur_rec->data;
        status = mail$send_add_bodypart 
			(&sendContext, &itemList, &nullItemList) ;
	if ( !$VMS_STATUS_SUCCESS(status) ) send_fail ( "500 mail error",
	    "MAIL$SEND_ADD_BODYPART returned %%X%0X", (void *) status) ;
    }
    /*
     * Message complete, queue to delivery system.
     */
    status =
	mail$send_message (
	    &sendContext,
	    &nullItemList,
	    &nullItemList) ;				/* Send the message.							 */
    if (!$VMS_STATUS_SUCCESS(status)) send_fail ( "500 mail error",
	"MAIL$SEND_MESSAGE returned %%X%0X", (void *) status) ;
	 
    status =
	mail$send_end (
	    &sendContext,
	    &nullItemList,
	    &nullItemList) ;	    			/* Set up to construct the mail context.				 */
    if (!$VMS_STATUS_SUCCESS(status)) send_fail ( "500 mail error",
	"MAIL$SEND_END returned %%X%0X", (void *) status);

    if ( success ) {
	cgi_printf ( "location: %s\n\n", success );
    } else if ( context.body[0] ) {
	/*
	 * Non-null body remaining means it holds data to return, assume
	 * it includes CGI headers.
	 */
	ent_flag = 1;
	while (  scan_body (&context, field_list, body_line, 
		sizeof(body_line)-1, &length ) > 0 ) {
	    cgi_printf ( "%s\n", body_line );
	}
    } else {
	cgi_printf("content-type: text/plain\n");	/* CGI header */
	if ( (succ_sts < 200) || (succ_sts > 299) ) succ_sts = 200;
	cgi_printf("status: %d mail delivered\n\n", succ_sts );
	cgi_printf("Sending form data as MAIL to: %s\n",to);
	cgi_printf("Send Succeeded: submitted form data mailed\n") ;
    }
    return 1;
}
/*****************************************************************************/
/* Scan template body for tags and expand from form fields data, return
 * result in line-oriented fashion.
 */
static int scan_body ( struct scanctx *context, field_def_p fields,
	char *buffer, int bufsize, int *length )
{
    int i;
    /*
     * Load next subline if none currently.
     */
    if ( !context->subline ) {
	/* scan next body line. */
	for ( i = 0; context->body[i] != '\n'; i++ ) {
	    if ( !context->body[i] ) return 0;	/* no more body */
	}
	context->body[i] = '\0';
	context->subline = expand_line ( context->body, fields );
	context->body[i] = '\n';
	context->body = &context->body[i+1];
	if ( !context->subline ) return 0;
    }
    /*
     * parse data out of subline.
     */
    for ( i = 0; i < bufsize; i++ ) {
	buffer[i] = context->subline[i];
	if ( buffer[i] == '\n' ) {
	    context->subline = &context->subline[i+1];
	    break;
	} else if ( context->subline[i] == '\r' ) {
	    if ( context->subline[i+1] == '\n' )
		context->subline = &context->subline[i+2];
	    else context->subline = &context->subline[i+1];
	    break;
	} else if ( !context->subline[i] ) {
	    context->subline += i;
	    break;
	}
    }
    buffer[i] = '\0';
    *length = i;
    if ( i >= bufsize ) context->subline = &context->subline[i];
    if ( context->subline[0] == '\0' ) { 
	/* Done processing subline, force load of next */
	context->subline = (char *) 0;
    }
    return 1;
}
/****************************************************************************/
/* Convert escaped characters in string */
static int htmlStrcpy (
    char *out,
    char *in)
{
    int	i, j;
    for ( j=0;  *in;  in++)
    {
    	if (*in == '%') {
	    int	xxx ;
	    if ( (in[1]=='\0') || (in[2]=='\0') ) break;  /* too short */
	    sscanf (&in[1], "%02x", &xxx) ;
	    out[j++] = (char) xxx ;
	    in += 2 ;
	} else {
	    out[j++] = *in;
	}
    }
    return j;
}
/****************************************************************************/
/* Read form data and parse into list of key/value pairs.  Plus-to-space
 * conversion is performed but escaped chars are not converted.
 * Full CGI responses are generated on error.
 */
static field_def_p form_data()
{
    char *method, *content_data, *cp;
    field_def_p field_list, fld;
    int i, content_length, status;
    /*
     * Determine what form data is being delivered by client.
     */
    method = cgi_info ( "REQUEST_METHOD" );
    if ( 0 == strcmp ( method, "POST" ) ) {
	/*
	 * Data was posted.
	 */
        content_length = atoi(cgi_info ("CONTENT_LENGTH"));
        if (content_length == 0) send_fail ( "500 missing content", 
		"No data in CONTENT_LENGTH (%s)", cgi_info("CONTENT_LENGTH") );

        content_data = (char *) malloc(sizeof(char)*(content_length+1));
        status = cgi_read(content_data, content_length);
        if (status == 0) send_fail ( "500 missing content", 
		"No data from cgi_read", 0 );

        content_data[content_length] = '\0';
    } else if ( 0 == strcmp ( method, "GET" ) ) {
	/*
	 * Form method was GET, data is supplied as query string.
	 */
	cp = cgi_info ( "QUERY_STRING" );
	if ( !cp ) send_fail ( "500 missing content",
	    "no data from cgi_read\n", 0 ) ;

	if ( *cp == '?' ) cp++;
	content_length = strlen ( cp );
	content_data = (char *) malloc ( content_length + 1 );
	strcpy ( content_data, cp );

	/* printf("GET Content: '%s' l: %d\n", content_data, content_length);*/
    } else {
	send_fail("501 unsupported method","Method %s is unsupported",
	    method );
    }
    /*
     * Parse the content into key/value pairs.
     */
    field_list = (field_def_p) 0;
    if ( content_data[0] != '&' ) {
	fld = (field_def_p) malloc ( sizeof(struct field_def) );
	fld->next = field_list;
	fld->key = content_data;
	fld->klen = 0;
	field_list = fld;
    }
    for ( i = 0; i < content_length; i++ ) if ( content_data[i] == '&' ) {
	fld = (field_def_p) malloc ( sizeof(struct field_def) );
	fld->next = field_list;
	fld->key = &content_data[i+1];
	fld->klen = 0;
	content_data[i] = '\0';
	field_list = fld;
    }
    for ( fld = field_list; fld; fld = fld->next ) {
	fld->value = "";
	for ( cp = fld->key, i = 0; cp[i]; i++ ) {
	    if ( cp[i] == '=' ) {
		cp[i] = '\0';
		fld->klen = i;
		fld->value = &cp[i+1];
	    }
	}
	for ( cp = fld->value; *cp; cp++ ) if ( *cp == '+' ) *cp = ' ';
	/* printf("Parsed key '%s' = '%s'\n", fld->key, fld->value ); */
    }

    return field_list;
}
/*****************************************************************************/
/* Scan line for tags ([tag-name]) and replace with the corresponding value
 * from flist or cgi_info for that tag.
 */
static char *expand_line ( char *line, field_def_p flist )
{
    char *new;
    field_def_p fld;
    int i, j, k, size, length, lbrack, rbrack;
    /*
     * Allocate buffer to hold result, use size+256 as initial guess.
     */
    size = 256 + strlen ( line );
    new = malloc ( size );
    /*
     * Scan string for tags.
     */
    for ( i = j = 0; line[i]; i++ ) {
	if ( line[i] == '[' ) {
	    /*
	     * Start of tag detected
	     */
	    if ( line[i+1] == '[' ) {
		/* Treat "[[" specially: insert single "[" */
		k = 1;
		if ( j+1 >= size ) {
		    size += 256;
		    new = realloc ( new, size );
		}
		new[j++] = '[';
	    } else if ( line[i+1] == ']' ) {
		/* Treat [] specially: insert single "]" */
		k = 1;
		if ( j+1 >= size ) {
		    size += 256;
		    new = realloc ( new, size );
		}
		new[j++] = ']';
	    } else for ( k = 1; line[i+k]; k++) if ( line[i+k] == ']' ) {
		/*
		 * Found end of tag.
		 */
		int saved_j = j;
		if ( line[i+1] == '%' && (k < 65) && (k > 1) ) {
		    /*
		     * string following % is CGI environment variable
		     * unless it is %end.
		     */
		    char varname[64], *var;
		    strncpy ( varname, &line[i+2], k-2 );
		    varname[k-2] = '\0';
		    if ( varname[0] == '%' ) {	/* special variable */
			int l;
			for (l=0; varname[l]; l++) 
				varname[l] = tolower(varname[l]);
			length = 0;
			if ( strcmp ( varname, "%end" ) == 0 ) {
			    /* Magic tag to force EOD */
			    free ( new );
			    return (char *) 0;
			} else if ( strcmp ( varname, "%noentify" ) == 0 ) {
			    ent_flag = 0;
			} else if ( strcmp ( varname, "%entify" ) == 0 ) {
			    ent_flag = 1;
			}
		    } else { 			/* CGI variable */
		        var = cgi_info ( varname );
		        if ( !var ) var = "%unknown";
		        if ( ent_flag ) var = entify_string ( var );
		        length = strlen(var);
		        if ( length+j+1 >= size ) {
		            size = j + length + 256;
		            new = realloc ( new, size );
		        }
		        strcpy ( &new[j], var );
		    }
		    j += length;
		} else {
		    /*
		     * Check tag for format specifiers:
		     *    tag:string	value is string or null
		     *    tag?string    value is string if field 'on'
		     */
		    int klen, fmt_flag;
		    for (klen=fmt_flag=0; klen<k-1; klen++ ) if ( 
			line[i+klen+1] == ':' || line[i+klen+1] == '?' ) {
			fmt_flag = 1; break;
		    }
		    /*
		     * Scan fields supplied by caller for match.
		     */
		    for (fld=flist; fld; fld=fld->next) if (fld->klen==klen ) {
		        if ( strncmp(&line[i+1],fld->key,klen) == 0 ) {
			    char *value;
			    value = fld->value;
			    if ( fmt_flag ) value = reformat_value ( value,
				&line[i+klen+1], k-1-klen );
			    length = strlen ( value );
			    if ( length + j+1 >= size ) {
			        size = j + length + 256;
			        new = realloc ( new, size );
			    }
			    length = htmlStrcpy ( &new[j], value );
			    if ( ent_flag ) {
				/* Convert <s and >s */
				value = entify_string(&new[j]);
				if ( value != &new[j] ) {
				    length = strlen ( value );
				    if ( length + j+1 >= size ) {
					size = j + length + 256;
					new = realloc ( new, size );
				    }
				    strcpy ( &new[j], value );
				}
			    }
			    j += length;
			    if ( fmt_flag ) break;	/* only expand once */
		        }
		    }
		}
		if ( saved_j = j ) if ( strncmp("required-",&line[i+1],9)==0 ) {
		    /*
		     * Required field was missing, aobrt with error message.
		     */
		    send_fail ( "500 error", 
			"Required field not supplied: %s", &line[i] );
		}
		break;
	    }
	    if ( line[i+k] != '\0' ) i += k;
	} else {
	    /*
	     * Add character not part of tags to output.
	     */
	    if ( j+1 >= size ) {
		size = size + 256;
		new = realloc ( new, size );
	    }
	    new[j++] = line[i];
	}
    }
    /*
     * Ensure result is terminated and return in to caller.
     */
    new[j] = '\0';
    return new;
}
/****************************************************************************/
/* Substitute value according to format string.
 */
static char *reformat_value ( char *value, char *format, int fmt_len )
{
    char *result;
    /*
     * Perform tests based upon first character of format string,
     * returning empty string on test failure.
     */
    result = "";
    if ( *format == '?' ) {
	/* test value for 'on' */
	if ( tolower(value[0]) != 'o' ) return result;
	if ( tolower(value[1]) != 'n' ) return result;
	if ( value[2] != '\0' ) return result;
    } else if ( *format == ':' ) {
	/* test for non-null */
	if ( !*value ) return result;
    }
    /*
     * Copy chars in format string after 1st to result string.
     */
    result = malloc ( fmt_len );
    if ( !result ) return "";
    strncpy ( result, &format[1], fmt_len-1 );
    result[fmt_len-1] = '\0';

    return result;
}
/****************************************************************************/
/* Convert strings containing punctuation characters into escaped strings.
 */
static char *entify_string ( char *source )
{
   int i, j,brack_count;
   char *dest;
   dest = source;
   brack_count = 0;
   for ( i = 0; source[i]; i++ ) {
	if ( (source[i] == '<') || (source[i] == '>') ||
	    (source[i] == '&') ) brack_count++;
   }
   if ( brack_count > 0 ) {
	dest = malloc ( i + brack_count*4 + 1 );
	if ( !dest ) return source;

	for ( i = j = 0; source[i]; i++ ) {
	    if ( source[i] == '<' ) {
		dest[j++] = '&';
		dest[j++] = 'l';
		dest[j++] = 't';
		dest[j++] = ';';
	    } else if ( source[i] == '>' ) {
		dest[j++] = '&';
		dest[j++] = 'g';
		dest[j++] = 't';
		dest[j++] = ';';
	    } else if ( source[i] == '&' ) {
		dest[j++] = '&';
		dest[j++] = 'a';
		dest[j++] = 'm';
		dest[j++] = 'p';
		dest[j++] = ';';
	    } else
		dest[j++] = source[i];
	}
	dest[j] = '\0';
   }
   return dest;
}
