/*****************************************************************************/
/*
                                tMAILer.c

This is a drop-in replacement for the OSU TMAIL script (original authors Dick
Monroe and David Jones) for the WASD environment.  Written from scratch, it is
intended to be backwardly compatible.  Being directly CGI process based it will
avoid that initial latency when a DECnet script is activated.

The concept is roughly this.  A form's ACTION= element specifies this script
and another file, a template file, as the path.  The script opens the template
file and uses it's contents to parse the form's field(s), generating and
sending a mail message.

If there is a large number of existing OSU-based TMAIL forms/templates in use
the following HTTPD$MAP rule will simply result in them being processed by
TMAILER instead.

  map /htbin/tmail/* /cgi-bin/tmailer/*


FORM EXAMPLE
------------

  |<FORM METHOD=POST ACTION="/cgi-bin/tmailer/web/example.tmail">
  |Comments will be mailed to Web administrator:
  |<TEXTAREA NAME=comments COLS=80 ROWS=8>
  |</TEXTAREA>
  |<INPUT TYPE=text NAME=email SIZE=40> ... your email address (confidential)
  |<BR><INPUT TYPE=submit VALUE=" Mail Comments ">
  |<INPUT TYPE=reset VALUE=" Clear ">
  |<BR>Thankyou.
  |</FORM>

An example template file for processing this form is shown below.


TEMPLATE FILE FORMAT
--------------------
The template file is plain text, and is commonly named with .TMAIL as the
extension (although it could be made anything).  It comprises at least two and
more often three sections.  The first, separated from the next by a blank line,
is termed the header section and contains a version, mail message and response
directives.  The first line must always be the "tmail:" version, as follows:

  tmail: 1.1

This ensures the file is intended as a template.  The next line should be the
destination of the mail message.  WASD-specific allows a comma-separated list
of addresses.  WASD-specific also allows a "To:" address to begin with a '?'. 
In this case a facsimile of the received message is returned to the client.
Another variant allows the address to begin with a '!', suppressing actual
mailing but continuing on to deliver a response to the client.  Both useful for
template "debugging" or demonstration purposes.

  to: address[,address2,address3]
  to: ?address
  to: !address

An optional subject line should be provided.

  subject: This is the subject of the "subject:" directive

Two more optional directives control the response to the request.

  success:  URL to redirect the client to after successful mailing
  success-status:  HTTP status code to return instead of 200

Another optional directive sets the "personal name" part of the source email
address of the message which TMAILer sends:

  personal: personal name string

If this directive is not supplied, a default name will be used based on the
name of the template file.

The second section of the template file (remember this is separated from the
header section by a blank, or empty, line) is the body section.  The text in
this section (plus any expanded tag information, see below) is what is mailed
to the destination address specified with the "to:" above.

The third, and completely optional section, allows a specific response to be
returned to the client.  The command tag "[%%end]" delimits the second section
from the third section. The third section should be in the format of a CGI
script response.  It should therefore begin with a "Content-Type: text/html",
an empty line, and then a response body.


TAGS
----
Tags mark points in the template where the contents of a form field or CGI
variable is inserted into the text.  Tags may be used anywhere within the
template file.  If a field or CGI variable does not exist the string "%unknown"
is returned.

The following characters are reserved and can be escaped by preceding them with
a '[' character, as listed here:

  '[' escape using "[["
  ']' escape using "[]"
  ':' escape using "[:"
  '?' escape using "[?"

Tag information is introduced using the '[' and concluded with ']' characters.

Three tag formats are supported:

  [field-name]      the data associated with the form field name
  [%cgi-var-name]   the contents of a CGI variable
  [%%command]       a directive to the template processor

Form field names inside tags are case-sensitive.  CGI variable names are not.


COMMAND TAGS
------------
Tags beginning with "%%" provide directives to the template processor.  Three
OSU and one WASD-specifc command tags are defined.

  [%%end]

Indicates the end of the mail message contents.  Any lines following are
considered to be a CGI-compatible response to be returned to request client. 
It should therefore begin with a "Content-Type: text/html", an empty line, and
then a response body.

  [%%entify]

Enables conversion of HTML forbidden characters (i.e. '<', '>', '&')  to the
corresponding HTML entity (i.e. "&lt;", "gt;", "&amp;").  By default this
conversion is disabled during mail message composition (i.e. (before a [%%end])
and enabled during any CGI response composition (i.e. after a [%%end]).

  [%%noentify]

Disables conversion to HTML entities, as described above.

  [%%reveal]

This is WASD-specific.  It displays the CGI variables and the form fields and
then exits, obviously for template "debugging" purposes.  The CGI variables are
shown with the prefixing "WWW_" which is optional when specifying tag field
names.  Place this immediately after the "tmail:" version directive.


CONDITIONAL EXPANSION
---------------------
Extensions to the tag format allows information other than the exact contents
of a tag field or CGI variable to be inserted based on the contents of that
field or variable!  Two OSU and two WASD-specific variants are available.

  [field-name:string]
  [%cgi-var-name:string]

If the form field or CGI variable contents are not null (i.e. the tag-name
exists and contains a non-empty string) the string following the ':' (which may
contain white-space, escaped characters, etc., but no nested tags) is inserted
into the text.  If the field or variable does not exist or is empty the string
is not inserted.

  [field-name:string1::string2]
  [%cgi-var-name:string1::string2]

This is WASD-specific.  Like the simple variant this one inserts the text of
string1 if the field name is not empty.  However if it is empty and a "::"
delimits a second string then that is inserted.

  [field-name?string]
  [%cgi-var-name?string]

If the form field or CGI variable contents contain the literal "on" (for
evaluating checkboxes) then the string following the '?' is inserted into the
text, otherwise it is not.

  [field-name?string1??string2]
  [%cgi-var-name?string1??string2]

This is WASD-specific.  Like the simple variant this one inserts the text of
string1 if the field name contains "on".  However if it is does not and a "??"
delimits a second string then that is inserted.


TEMPLATE FILE EXAMPLE
---------------------
The following example template file provides all three sections, header, body
and response (the '|' just indicates the limits of the file):  This template
could be used to process the form in the example above.

  |tmail: 1.1
  |to: WEBADMIN
  |subject: This is just an example (from [%SERVER_HOST])!
  |
  |This is the body of the message mailed to the above address.
  |The message was sent from [%HOST_ADDR]
  |by a client using "[%USER_AGENT]".
  |
  |The form contained a field named "comments", it's contents are:
  |---------------------------------------------------------------
  |[comments]
  |---------------------------------------------------------------
  |
  |The field "email" contains "[email]"
  |and [email:was::was not] completed by the client.
  |
  |This is the end of the mail message body.
  |What follows after the "[[%%end[]" is a CGI response.
  |[%%end]
  |Content-type: text/html
  |
  |<!-- the above blank line is the HTTP header separator -->
  |<HTML>
  |The message was successfully mailed.  Thankyou for your comments.
  |</HTML>


OSU NOTE!
---------
Although this code has build capabilities for the OSU environment it is not
intended to encroach on functionality already present in the OSU package.  I
have this in here merely for testing the CGILIB POSTed body processing
functionality.


QUALIFIERS
----------
/CHARSET=        "Content-Type: text/html; charset=..."
/DBUG            turns on all "if (Debug)" statements
/SOFTWAREID      (and /VERSION) display TMAILER and CGILIB versions


LOGICAL NAMES
-------------
TMAILER$DBUG       turns on all "if (Debug)" statements
TMAILER$PARAM      equivalent to (overrides) the command line
                   parameters/qualifiers (define as a system-wide logical)


BUILD DETAILS
-------------
See BUILD_ONE.COM procedure.


COPYRIGHT
---------
Copyright (C) 1999-2022 Mark G.Daniel

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.


VERSION HISTORY (update SOFTWAREVN as well!)
---------------
03-APR-2022  MGD  v1.3.5, bugfix; test for NULL after CgiLibReadRequestBody()
10-SEP-2009  MGD  v1.3.4, break rather than truncate body lines at 255
23-DEC-2003  MGD  v1.3.3, minor conditional mods to support IA64
28-NOV-2001  JMB  v1.3.2, add 'personal' directive
01-JUL-2001  MGD  v1.3.1, add 'SkipParameters' for direct OSU support
28-OCT-2000  MGD  v1.3.0, use CGILIB object module,
                          support RELAXED_ANSI compilation
12-APR-2000  MGD  v1.2.1, minor changes for CGILIB 1.4
07-AUG-1999  MGD  v1.2.0, use more of the CGILIB functionality
24-APR-1999  MGD  v1.1.0, use CGILIB.C
31-MAR-1999  MGD  v1.0.1, a couple of wrinkles
27-MAR-1999  MGD  v1.0.0, initial development
*/
/*****************************************************************************/

#define SOFTWAREVN "1.3.5"
#define SOFTWARENM "TMAILER"
#ifdef __ALPHA
#  define SOFTWAREID SOFTWARENM " AXP-" SOFTWAREVN
#endif
#ifdef __ia64
#  define SOFTWAREID SOFTWARENM " IA64-" SOFTWAREVN
#endif
#ifdef __VAX
#  define SOFTWAREID SOFTWARENM " VAX-" SOFTWAREVN
#endif
#ifdef __x86_64
#  define SOFTWAREID SOFTWARENM " X86-" SOFTWAREVN
#endif

/* standard C header files */
#include <ctype.h>
#include <errno.h>
#include <lib$routines.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unixio.h>

/* VMS-related header files */
#include <descrip.h>
#include <maildef.h>
#include <ssdef.h>
#include <stsdef.h>
#include <mail$routines.h>

#define boolean int
#define true 1
#define false 0

/* application related header file */
#include "cgilib.h"

char  CopyrightInfo [] =
"Copyright (C) 2005-2009 Mark G.Daniel.\n\
This program, comes with ABSOLUTELY NO WARRANTY.\n\
This is free software, and you are welcome to redistribute it under the\n\
conditions of the GNU GENERAL PUBLIC LICENSE, version 3, or any later version.\n\
http://www.gnu.org/licenses/gpl.txt\n";

#ifndef __VAX
#   pragma nomember_alignment
#endif

#define VMSok(x) ((x) & STS$M_SUCCESS)
#define VMSnok(x) (!((x) & STS$M_SUCCESS))
 
#define FI_LI __FILE__, __LINE__

#define TMAIL_VERSION "1.1"

#define FORM_URLENCODED "application/x-www-form-urlencoded"

#define UNKNOWN_TAG "%unknown"

#define ISLWS(c) (c == ' ' || c == '\t')

char  Utility [] = "TMAILER";

boolean  Debug,
         CommandTagEnd,
         CommandTagEntify;

int  BufferCount,
     CgiResponseLength,
     CgiResponseOffset,
     HeaderTmailOffset,
     HeaderToOffset,
     HeaderSubjectOffset,
     HeaderSuccessOffset,
     HeaderSuccessStatusOffset,
     HeaderPersonalOffset,
     ParsedTemplateLength,
     RequestDataLength,
     TemplateBodyLength,
     TemplateBodyOffset,
     ThereHasBeenOutput;

char  *BufferPtr,
      *CharsetPtr,
      *CgiContentTypePtr,
      *CgiEnvironmentPtr,
      *CgiPathInfoPtr,
      *CgiPathTranslatedPtr,
      *CgiRequestMethodPtr,
      *CgiResponsePtr,
      *CgiServerSoftwarePtr,
      *CliCharsetPtr,
      *HeaderToPtr,
      *HeaderSubjectPtr,
      *HeaderSuccessPtr,
      *HeaderSuccessStatusPtr,
      *HeaderPersonalPtr,
      *ParsedTemplatePtr,
      *RequestDataPtr,
      *TemplateBodyPtr;
      
char  CharsetString [64],
      ContentTypeCharset [64],
      SoftwareID [48];

/* required function prototypes */
char* ConditionalExpansion (char*, char*);
char* TagCgiVar (char*, char*);
char* TagFormCgiVar (char*, char*);

/*****************************************************************************/
/*
'argc/argv' are only required to support OSU, in particular CgiLibOsuInit().
*/

main
(
int argc,
char *argv[]
)
{
   /*********/
   /* begin */
   /*********/

   sprintf (SoftwareID, "%s (%s)", SOFTWAREID, CgiLibEnvironmentVersion());

   if (getenv ("TMAILER$DBUG") != NULL) Debug = true;
   if (Debug) fprintf (stdout, "Content-Type: text/plain\n\n");
   CgiLibEnvironmentSetDebug (Debug);

   GetParameters ();

   CgiLibEnvironmentInit (argc, argv, false);

   CgiLibResponseSetCharset (CliCharsetPtr);
   CgiLibResponseSetSoftwareID (SoftwareID);
   CgiLibResponseSetErrorMessage ("Reported by TMailer");

   CgiEnvironmentPtr = CgiLibEnvironmentName ();

   ProcessRequest ();

   exit (SS$_NORMAL);
}

/*****************************************************************************/
/*
Get "command-line" parameters, whether from the command-line or from a
configuration symbol or logical containing the equivalent.  OSU scripts have
the 'method', 'url' and 'protocol' supplied as P1, P2, P3 (these being detected
and used by CGILIB), and are of no interest to this function.
*/

GetParameters ()

{
   static char  CommandLine [256];
   static unsigned long  Flags = 0;

   int  status,
        SkipParameters;
   unsigned short  Length;
   char  ch;
   char  *aptr, *cptr, *clptr, *sptr;
   $DESCRIPTOR (CommandLineDsc, CommandLine);

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GetParameters()\n");

   if ((clptr = getenv ("TMAILER$PARAM")) == NULL)
   {
      /* get the entire command line following the verb */
      if (VMSnok (status =
          lib$get_foreign (&CommandLineDsc, 0, &Length, &Flags)))
         exit (status);
      (clptr = CommandLine)[Length] = '\0';
   }

   /* if OSU environment then skip P1, P2, P3 */
   if (getenv ("WWWEXEC_RUNDOWN_STRING") != NULL)
      SkipParameters = 3;
   else
      SkipParameters = 0;

   aptr = NULL;
   ch = *clptr;
   for (;;)
   {
      if (aptr != NULL && *aptr == '/') *aptr = '\0';
      if (!ch) break;

      *clptr = ch;
      if (Debug) fprintf (stdout, "clptr |%s|\n", clptr);
      while (*clptr && isspace(*clptr)) *clptr++ = '\0';
      aptr = clptr;
      if (*clptr == '/') clptr++;
      while (*clptr && !isspace (*clptr) && *clptr != '/')
      {
         if (*clptr != '\"')
         {
            clptr++;
            continue;
         }
         cptr = clptr;
         clptr++;
         while (*clptr)
         {
            if (*clptr == '\"')
               if (*(clptr+1) == '\"')
                  clptr++;
               else
                  break;
            *cptr++ = *clptr++;
         }
         *cptr = '\0';
         if (*clptr) clptr++;
      }
      ch = *clptr;
      if (*clptr) *clptr = '\0';
      if (Debug) fprintf (stdout, "aptr |%s|\n", aptr);
      if (!*aptr) continue;

      if (SkipParameters)
      {
         SkipParameters--;
         continue;
      }

      if (strsame (aptr, "/CHARSET=", 4))
      {
         for (cptr = aptr; *cptr && *cptr != '='; cptr++);
         if (*cptr) cptr++;
         CliCharsetPtr = cptr;
         continue;
      }
      if (strsame (aptr, "/DBUG", -1))
      {
         Debug = true;
         continue;
      }
      if (strsame (aptr, "/SOFTWAREID", 4) ||
          strsame (aptr, "/VERSION", 4))
      {
         fprintf (stdout, "%%%s-I-SOFTWAREID, %s\n%s\n",
                  Utility, SoftwareID, CopyrightInfo);
         exit (SS$_NORMAL);
      }

      if (*aptr == '/')
      {
         fprintf (stdout, "%%%s-E-IVQUAL, unrecognized qualifier\n \\%s\\\n",
                  Utility, aptr+1);
         exit (STS$K_ERROR | STS$M_INHIB_MSG);
      }

      fprintf (stdout, "%%%s-E-MAXPARM, too many parameters\n \\%s\\\n",
               Utility, aptr);
      exit (STS$K_ERROR | STS$M_INHIB_MSG);
   }
}

/*****************************************************************************/
/*
*/

ProcessRequest ()

{
   int  status,
        StatusCode,
        TemplateLength;
   char  *cptr,
         *TemplatePtr;
   char  PersonalName [256];

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ProcessRequest()\n");

   CgiServerSoftwarePtr = CgiLibVar ("WWW_SERVER_SOFTWARE");
   CgiPathInfoPtr = CgiLibVar ("WWW_PATH_INFO");
   CgiPathTranslatedPtr = CgiLibVar ("WWW_PATH_TRANSLATED");

   status = ReadFileIntoMemory (CgiPathTranslatedPtr,
                                &TemplatePtr, &TemplateLength);
   if (VMSnok (status))
   {
      CgiLibResponseError (FI_LI, status, CgiPathInfoPtr);
      exit (SS$_NORMAL);
   }

   CheckTemplateVersion (TemplatePtr);

   if (RequestDataPtr == NULL)
      CgiLibReadRequestBody (&RequestDataPtr, &RequestDataLength);

   if (RequestDataPtr == NULL)
   {
      CgiLibResponseError (FI_LI, 0, "Must be URL-encoded form data!");
      exit (SS$_NORMAL);
   }

   if (Debug) fprintf (stdout, "RequestDataPtr ...\n|%s|\n", RequestDataPtr);

   ParseTemplate (TemplatePtr);

   ResolvePointers ();

   if (HeaderPersonalPtr)
      strcpy (PersonalName, HeaderPersonalPtr);
   else
      sprintf (PersonalName, "tmailer: %s", CgiPathInfoPtr);

   if (HeaderToPtr[0] == '?')
   {
      /* a question mark allows viewing the message without actual mailing! */
      CgiLibResponseHeader (200, "text/plain");
      RevealMailMessage (PersonalName, HeaderToPtr,
                         HeaderSubjectPtr, TemplateBodyPtr);
      exit (SS$_NORMAL);
   }
   else
   if (HeaderToPtr[0] != '!')
   {
      /* an exclamation point allows testing without actual mailing! */
      MailMessage (PersonalName, HeaderToPtr,
                   HeaderSubjectPtr, TemplateBodyPtr);
   }

   if (CgiResponsePtr == NULL)
   {
      if (HeaderSuccessStatusPtr[0])
      {
         for (cptr = HeaderSuccessStatusPtr; *cptr && !isdigit(*cptr); cptr++);
         StatusCode = atoi(cptr);
      }
      if (StatusCode < 200 || StatusCode > 299) StatusCode = 200;

      if (HeaderSuccessPtr[0])
      {
         /* redirection */
         CgiLibResponseRedirect (HeaderSuccessPtr);
      }
      else
      {
         CgiLibResponseHeader (200, "text/html");
         fprintf (stdout,
"<HTML>\n\
<HEAD>\n\
<META NAME=\"generator\" CONTENT=\"%s\">\n\
<TITLE>Message Mailed!</TITLE>\n\
</HEAD>\n\
<BODY>\n\
Sending form data as MAIL to: %s\n\
</BODY>\n\
</HTML>\n",
            SoftwareID, HeaderToPtr);
      }
   }
   else
      fprintf (stdout, "%s", CgiResponsePtr);
}

/*****************************************************************************/
/*
Read the request body (if POST) or the request's query string (if GET) into a
single array of char (doesn't matter whether it's text or binary) pointed to by
'BufferPtr' with a length of 'BufferCount'. If the NET$LINK environment
variable exists (DECnet CGI input/output) then read from that in record mode
(it's DECnet after all).  For DECnet an empty record (newline character only)
terminates the request body, for subprocess CGI an EOF is read.
*/

ReadRequestDataIntoMemory
(
char **BufferPtrPtr,
int *DataSizePtr
)
{
   static int  BufferChunk = 1024;

   static int  BufferCount;
   static char  *BufferPtr;

   int  BufferSize,
        ReadCount;
   char  *cptr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ReadRequestDataIntoMemory()\n");

   /* if the body has already been read then just return */
   if (BufferPtr != NULL)
   {
      if (BufferPtrPtr != NULL) *BufferPtrPtr = BufferPtr;
      if (DataSizePtr != NULL) *DataSizePtr = BufferCount;
      return;
   }

   CgiRequestMethodPtr = CgiLibVar("WWW_REQUEST_METHOD");
   if (strsame (CgiRequestMethodPtr, "GET", -1))
   {
      BufferPtr = CgiLibVar("WWW_QUERY_STRING");
      BufferCount = strlen (BufferPtr);
      if (BufferPtrPtr != NULL) *BufferPtrPtr = BufferPtr;
      if (DataSizePtr != NULL) *DataSizePtr = BufferCount;
      return;
   }
   else
   if (strsame (CgiRequestMethodPtr, "POST", -1))
   {
      CgiContentTypePtr = CgiLibVar("WWW_CONTENT_TYPE");
      if (!strsame (CgiContentTypePtr, FORM_URLENCODED, -1))
      {
         CgiLibResponseError (FI_LI, 0, "Must be URL-encoded form data!");
         exit (SS$_NORMAL);
      }
   }
   else
   {
      CgiLibResponseError (FI_LI, 0,
         "HTTP method must be \"GET\" or \"POST\"!");
      exit (SS$_NORMAL);
   }

   CgiLibReadRequestBody (&BufferPtr, &BufferCount);

   if (BufferPtrPtr != NULL) *BufferPtrPtr = BufferPtr;
   if (DataSizePtr != NULL) *DataSizePtr = BufferCount;
}

/****************************************************************************/
/*
Read the file contents specified by 'FileName' into memory, set the pointer
at 'FileTextPtr' to the contents and the file size at 'FileSizePtr'.  Returns a
VMS status value that should be checked.
*/ 

int ReadFileIntoMemory
(
char *Source,
char **FileTextPtr,
int *FileSizePtr
)
{
   static int  BytesRemaining;

   int  status,
        Bytes,
        BufferCount,
        Length;
   char  *BufferPtr,
         *LinePtr;
   FILE  *FilePtr;
   stat_t  FstatBuffer;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ReadFileIntoMemory() |%s|\n", Source);

   if ((FilePtr = fopen (Source, "r", "shr=get", "shr=put")) == NULL)
   {
      status = vaxc$errno;
      if (Debug) fprintf (stdout, "fopen() %%X%08.08X\n", status);
      return (status);
   }

   if (fstat (fileno(FilePtr), &FstatBuffer) < 0)
   {
      status = vaxc$errno;
      if (Debug) fprintf (stdout, "fstat() %%X%08.08X\n", status);
      fclose (FilePtr);
      return (status);
   }

   Bytes = FstatBuffer.st_size;
   if (Debug) fprintf (stdout, "%d bytes\n", Bytes);
   /* a little margin for error ;^) */
   Bytes += 32;

   BufferPtr = calloc (Bytes, 1);
   if (BufferPtr == NULL)
   {
      CgiLibResponseError (FI_LI, vaxc$errno, "calloc()");
      exit (SS$_NORMAL);
   }
   BytesRemaining = Bytes;
   LinePtr = BufferPtr;

   BufferCount = 0;
   while (fgets (LinePtr, BytesRemaining, FilePtr) != NULL)
   {
      /** if (Debug) fprintf (stdout, "|%s|\n", LinePtr); **/
      Length = strlen(LinePtr);
      LinePtr += Length;
      BufferCount += Length;
      BytesRemaining -= Length;
   }
   fclose (FilePtr);

   if (Debug) fprintf (stdout, "%d |%s|\n", BufferCount, BufferPtr);

   if (FileTextPtr != NULL) *FileTextPtr = BufferPtr;
   if (FileSizePtr != NULL) *FileSizePtr = BufferCount;

   return (SS$_NORMAL);
}

/*****************************************************************************/
/*
Simply check the template file version.
*/

CheckTemplateVersion (char *TextPtr)

{
   char  *cptr;

   /*********/
   /* begin */
   /*********/

   if (Debug)
      fprintf (stdout, "CheckTemplateVersion() |%*.*s|\n", 10, 10, TextPtr);

   for (cptr = TextPtr; *cptr && (*cptr == ' ' || *cptr == '\t'); cptr++);
   if (!strsame (cptr, "tmail:", 6))
   {
      CgiLibResponseError (FI_LI, 0, "\"tmail:\"? Not a template file?");
      exit (SS$_NORMAL);
   }
   cptr += 6;
   while (*cptr && (*cptr == ' ' || *cptr == '\t')) cptr++;
   if (!strsame (cptr, TMAIL_VERSION, 3))
   {
      CgiLibResponseError (FI_LI, 0, "Template file version mismatch.");
      exit (SS$_NORMAL);
   }
}

/*****************************************************************************/
/*
Creates a large, single, dynamically allocated, null-terminated text string
based on the contents of the template file.  The template is parsed from
beginning to end expanding tags, etc.  As the parse progresses the various
components from the template header, body (mail message) and any CGI response
are identified and the offsets placed in global storage.  These will later be
used to generate pointers and otherwise reprocess the parsed contents (by
ResolvePointers()).
*/ 

ParseTemplate (char* String)

{
#define EXPAND_PARSE_MEMORY \
{ \
   Count = sptr - ParsedTemplatePtr; \
   if ((ParsedTemplatePtr = \
        realloc (ParsedTemplatePtr, BufferSize+BufferChunk+1)) == NULL) \
   { \
      CgiLibResponseError (FI_LI, vaxc$errno, "realloc()"); \
      exit (SS$_NORMAL); \
   } \
   BufferSize += BufferChunk; \
   zptr = ParsedTemplatePtr + BufferSize; \
   sptr = ParsedTemplatePtr + Count; \
}

   /***********/
   /* storage */
   /***********/

   static int  BufferChunk = 1024;

   boolean  InTemplateHeader;
   int  BufferSize,
        Count;
   char  *cptr, *sptr, *tptr, *zptr,
         *TagNameEndCharPtr;
   char  ErrorString [256],
         TagName [256];

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "ParseTemplate() |%s|\n", String);

   InTemplateHeader = true;
   CgiResponseOffset =
      CgiResponseLength =
      HeaderToOffset =
      HeaderSubjectOffset =
      HeaderSuccessOffset =
      HeaderSuccessStatusOffset =
      HeaderPersonalOffset =
      ParsedTemplateLength =
      TemplateBodyOffset = 0;

   if ((sptr = ParsedTemplatePtr =
        calloc (BufferSize = BufferChunk, 1)) == NULL)
   {
      CgiLibResponseError (FI_LI, vaxc$errno, "calloc()");
      exit (SS$_NORMAL);
   }
   zptr = ParsedTemplatePtr + BufferSize;

   cptr = String;
   while (*cptr)
   {
      if (InTemplateHeader)
      {
         if (*(unsigned short*)cptr == '\n\n')
         {
            TemplateBodyOffset = sptr - ParsedTemplatePtr + 2;
            InTemplateHeader = false;
         }
         else
         if (strsame (cptr, "Tmail:", 6))
            HeaderTmailOffset = sptr - ParsedTemplatePtr + 6;
         else
         if (strsame (cptr, "To:", 3))
            HeaderToOffset = sptr - ParsedTemplatePtr + 3;
         else
         if (strsame (cptr, "Subject:", 8))
            HeaderSubjectOffset = sptr - ParsedTemplatePtr + 8;
         else
         /* must come before "Success:" for the obvious reason */
         if (strsame (cptr, "Success-status:", 15))
            HeaderSuccessStatusOffset = sptr - ParsedTemplatePtr + 15;
         else
         if (strsame (cptr, "Success:", 8))
            HeaderSuccessOffset = sptr - ParsedTemplatePtr + 8;
         else
         if (strsame (cptr, "Personal:", 9))
            HeaderPersonalOffset = sptr - ParsedTemplatePtr + 9;
      }

      switch (*cptr)
      {
         case '[' :

            /*******/
            /* tag */
            /*******/

            if (*(unsigned short*)cptr == '[[')
            {
               /* escaped "[" */
               if (sptr >= zptr) EXPAND_PARSE_MEMORY;
               cptr++;
               *sptr++ = *cptr++;
               continue;
            }

            if (*(unsigned short*)cptr == '[]')
            {
               /* escaped "]" */
               if (sptr >= zptr) EXPAND_PARSE_MEMORY;
               cptr++;
               *sptr++ = *cptr++;
               continue;
            }

            cptr++;

            if (*(unsigned short*)cptr == '%%')
            {
               /***************/
               /* command tag */
               /***************/

               if (strsame (cptr, "%%end]", 6))
               {
                  cptr += 6;
                  CommandTagEnd = CommandTagEntify = true;
                  while (*cptr && *cptr != '\n') cptr++;
                  if (*cptr) cptr++;
                  CgiResponseOffset = sptr - ParsedTemplatePtr;
                  continue;
               }
               if (strsame (cptr, "%%entify]", 9))
               {
                  cptr += 9;
                  CommandTagEntify = true;
                  continue;
               }
               if (strsame (cptr, "%%noentify]", 11))
               {
                  cptr += 11;
                  CommandTagEntify = false;
                  continue;
               }
               if (strsame (cptr, "%%reveal]", 9))
               {
                  /* special case, reveal all available data */
                  fprintf (stdout, "%s\n\nCGI variables:\n\n", SoftwareID);
                  fflush (stdout);
                  system ("show symbol www_*");
                  fprintf (stdout, "\nForm fields:\n\n");
                  RevealFormFields ();
                  exit (SS$_NORMAL);
               }

               for (sptr = cptr; *sptr && *sptr != '\n'; sptr++);
               *sptr = '\0';
               sprintf (ErrorString, "Unknown command tag: \"[%s\"",
                        (char*)CgiLibHtmlEscape(cptr, -1, NULL, -1));
               CgiLibResponseError (FI_LI, 0, String);
               exit (SS$_NORMAL);
            }

            if (*cptr == '%')
            {
               /****************/
               /* CGI variable */
               /****************/

               cptr++;
               cptr += GetTagName (cptr, TagName, sizeof(TagName),
                                   &TagNameEndCharPtr);
               tptr = TagCgiVar (TagName, TagNameEndCharPtr);
               while (*tptr)
               {
                  if (sptr >= zptr) EXPAND_PARSE_MEMORY;
                  *sptr++ = *tptr++;
               }

               continue;
            }

            /**************/
            /* field name */
            /**************/

            cptr += GetTagName (cptr, TagName, sizeof(TagName),
                                &TagNameEndCharPtr);
            tptr = TagFormCgiVar (TagName, TagNameEndCharPtr);
            while (*tptr)
            {
               if (sptr >= zptr) EXPAND_PARSE_MEMORY;
               *sptr++ = *tptr++;
            }

            continue;

         /********************/
         /* just a character */
         /********************/

         default :

            if (sptr >= zptr) EXPAND_PARSE_MEMORY;
            *sptr++ = *cptr++;
            continue;

      }
   }
   *sptr = '\0';

   ParsedTemplateLength = sptr - ParsedTemplatePtr;

   CgiResponseLength = ParsedTemplateLength - CgiResponseOffset;

   TemplateBodyLength = ParsedTemplateLength - TemplateBodyOffset -
                        CgiResponseLength - 1;
}

/*****************************************************************************/
/*
During the parse of the template a number of byte-count offsets into the
dynamically allocated parsed text are set representing the starting locations
of strings of various interests.  Now the parsing and realloc()ing are over
turn these into pointers to null-terminated strings.
*/

ResolvePointers ()

{
   char  *cptr;

   /*********/
   /* begin */
   /*********/

   if (Debug)
      fprintf (stdout, "ResolvePointers() %d %d %d %d %d %d %d %d %d\n",
               HeaderToOffset, HeaderSubjectOffset,
               HeaderSuccessOffset, HeaderSuccessStatusOffset,
               HeaderPersonalOffset,
               TemplateBodyOffset, TemplateBodyLength,
               CgiResponseOffset, CgiResponseLength);

   if (HeaderToOffset)
   {
      cptr = ParsedTemplatePtr + HeaderToOffset;
      while (*cptr && ISLWS(*cptr)) cptr++;
      HeaderToPtr = cptr;
      while (*cptr && *cptr != '\n') cptr++;
      *cptr = '\0';
   }
   else
      HeaderToPtr = "";

   if (HeaderSubjectOffset)
   {
      cptr = ParsedTemplatePtr + HeaderSubjectOffset;
      while (*cptr && ISLWS(*cptr)) cptr++;
      HeaderSubjectPtr = cptr;
      while (*cptr && *cptr != '\n') cptr++;
      *cptr = '\0';
   }
   else
      HeaderSubjectPtr = "";

   if (HeaderSuccessOffset)
   {
      cptr = ParsedTemplatePtr + HeaderSuccessOffset;
      while (*cptr && ISLWS(*cptr)) cptr++;
      HeaderSuccessPtr = cptr;
      while (*cptr && *cptr != '\n') cptr++;
      *cptr = '\0';
   }
   else
      HeaderSuccessPtr = "";

   if (HeaderSuccessStatusOffset)
   {
      cptr = ParsedTemplatePtr + HeaderSuccessStatusOffset;
      while (*cptr && ISLWS(*cptr)) cptr++;
      HeaderSuccessStatusPtr = cptr;
      while (*cptr && *cptr != '\n') cptr++;
      *cptr = '\0';
   }
   else
      HeaderSuccessStatusPtr = "";

   if (HeaderPersonalOffset)
   {
      cptr = ParsedTemplatePtr + HeaderPersonalOffset;
      while (*cptr && ISLWS(*cptr)) cptr++;
      HeaderPersonalPtr = cptr;
      while (*cptr && *cptr != '\n') cptr++;
      *cptr = '\0';
   }
   else
      HeaderPersonalPtr = 0;

   if (TemplateBodyOffset)
   {
      TemplateBodyPtr = ParsedTemplatePtr + TemplateBodyOffset;
      TemplateBodyPtr[TemplateBodyLength] = '\0';
   }
   else
   {
      CgiLibResponseError (FI_LI, 0, "No template body found.");
      exit (SS$_NORMAL);
   }

   if (CgiResponseOffset)
      CgiResponsePtr = ParsedTemplatePtr + CgiResponseOffset;
   else
      CgiResponsePtr = NULL;

   if (Debug)
      fprintf (stdout, "|%s|\n|%s|\n|%s|\n|%s|\n|%s|\n|%s|\n",
               HeaderToPtr, HeaderSubjectPtr,
               HeaderSuccessPtr, HeaderSuccessStatusPtr,
               TemplateBodyPtr, CgiResponsePtr);
}

/*****************************************************************************/
/*
Parse a [...] delimited tag name from the string.  Any of ':', '?' or ']' will
terminate the tag name.  Set the 'TagNameEndCharPtrPtr' to the address of this
terminator.
*/ 

int GetTagName
(
char *String,
char *BufferPtr,
int SizeOfBuffer,
char **TagNameEndCharPtrPtr
)
{
   char  *cptr, *sptr, *zptr;
   char  ErrorString [256];

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "GetTagName()\n");

   zptr = (sptr = BufferPtr) + SizeOfBuffer;
   for (cptr = String;
        *cptr && *cptr != ']' && *cptr != ':' && *cptr != '?' && sptr < zptr;
        *sptr++ = *cptr++);
   {
      /* allow for escaped characters */
      if (*(unsigned short*)cptr == '[]')
         cptr++;
      else
      if (*(unsigned short*)cptr == '[:')
         cptr++;
      else
      if (*(unsigned short*)cptr == '[?')
         cptr++;
   }
   if (sptr < zptr && (*cptr == ']' || *cptr == ':' || *cptr == '?'))
   {
      *sptr = '\0';
      if (Debug) fprintf (stdout, "|%s|\n", BufferPtr);
      *TagNameEndCharPtrPtr = cptr;
      if (*cptr != ']') 
      {
         while (*cptr && *cptr != ']')
         {
            if (*(unsigned short*)cptr == '[]') cptr++;
            cptr++;
         }
      }
      if (*cptr) cptr++;
      return (cptr - String);
   }

   sprintf (ErrorString, "Tag problem: \"%s\"",
            (char*)CgiLibHtmlEscape(String, -1, NULL, -1));
   CgiLibResponseError (FI_LI, 0, ErrorString);
   exit (SS$_NORMAL);
}

/*****************************************************************************/
/*
Get the CGI variable value represented by the tag name.  Requires prefixing the
variable name with "WWW_".  Do a conditional expansion on the variable value.
*/ 

char* TagCgiVar
(
char *VarName,
char *TagNameEndCharPtr
)
{
   static char WwwVarName [256] = "WWW_";

   int  Length;
   char  *cptr, *sptr, *zptr;

   /*********/
   /* begin */
   /*********/

   if (Debug)
      fprintf (stdout, "TagCgiVar() |%s| %c\n", VarName, *TagNameEndCharPtr);

   cptr = CgiLibVar (VarName);

   cptr = ConditionalExpansion (cptr, TagNameEndCharPtr);

   if (CommandTagEntify)
   {
      sptr = (char*)CgiLibHtmlEscape (cptr, -1, NULL, -1);
      free (cptr);
      cptr = sptr;
   }

   if (Debug) fprintf (stdout, "%s |%s|\n", WwwVarName, cptr);
   return (cptr);
}

/*****************************************************************************/
/*
Get the form-URL-encoded form field value.  This will have come from the query
string if a "GET" request or the request body if a "POST" request.  Both will
the same format, a URL-encoded, null-terminated string.  Do a conditional
expansion on the variable value.
*/ 

char* TagFormCgiVar
(
char *VarName,
char *TagNameEndCharPtr
)
{
   int  Length;
   char  *cptr, *sptr, *zptr;

   /*********/
   /* begin */
   /*********/

   if (Debug)
      fprintf (stdout, "TagFormCgiVar() |%s| %c\n",
               VarName, *TagNameEndCharPtr);

   cptr = RequestDataPtr;
   for (;;)
   {
      if (!*cptr) break;
      for (sptr = cptr; *sptr && *sptr != '='; sptr++);
      if (!*sptr)
      {
         cptr = "";
         break;
      }
      *sptr = '\0';
      if (Debug) fprintf (stdout, "|%s|\n", cptr);

      /* case-sensitive compare for field names */
      if (strcmp (cptr, VarName))
      {
         *sptr++ = '=';
         for (cptr = sptr; *cptr && *cptr != '&'; cptr++);
         if (*cptr) cptr++;
         continue;
      }

      *sptr++ = '=';
      cptr = sptr;
      while (*sptr && *sptr != '&') sptr++;
      Length = sptr - cptr;
      if ((sptr = calloc (Length+1, 1)) == NULL)
      {
         CgiLibResponseError (FI_LI, vaxc$errno, "calloc()");
         exit (SS$_NORMAL);
      }
      memcpy (sptr, cptr, Length);
      sptr[Length] = '\0';

      /* now URL-decode in-situ */
      CgiLibUrlDecode (cptr = sptr);
      break;
   }

   cptr = ConditionalExpansion (cptr, TagNameEndCharPtr);

   if (CommandTagEntify)
   {
      sptr = (char*)CgiLibHtmlEscape (cptr, -1, NULL, -1);
      free (cptr);
      cptr = sptr;
   }

   if (Debug) fprintf (stdout, "|%s|\n", cptr);
   return (cptr);
}

/*****************************************************************************/
/*
Just display each form field as 'name == "value"' (for template debugging).
*/ 

RevealFormFields ()

{
   char  *cptr, *sptr;

   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "RevealFormFields()\n");

   cptr = RequestDataPtr;
   while (*cptr)
   {
      for (sptr = cptr; *sptr && *sptr != '='; sptr++)
      if (!*sptr) break;
      *sptr++ = '\0';
      fprintf (stdout, "  %s == \"", cptr);
      for (cptr = sptr; *cptr && *cptr != '&'; cptr++);
      if (*cptr) *cptr++ = '\0';
      /* now URL-decode in-situ */
      CgiLibUrlDecode (sptr);
      fprintf (stdout, "%s\"\n", sptr);
   }
}

/*****************************************************************************/
/*
If the tag name ended with a ':' and the tag value was empty (null) then the
and empty string is returned.  If the tag value was non-empty the text
following the ':' up to the next ']' is returned.  If the tag name ended with a
'?' and the tag value was "on" (case-insensitive) then the text following the
'?' and up to the next ']' is returned.  If not "on" an empty string is
returned.
*/ 

char* ConditionalExpansion
(
char *TagValue,
char *TagNameEndCharPtr
)
{
   int  Length;
   char  *cptr, *sptr, *vptr;

   /*********/
   /* begin */
   /*********/

   if (Debug)
      fprintf (stdout, "ConditionalExpansion() |%s| %c\n",
               TagValue, *TagNameEndCharPtr);

   if (*TagNameEndCharPtr == ':')
   {
      TagNameEndCharPtr++;
      if (TagValue != NULL && TagValue[0])
      {
         for (sptr = TagNameEndCharPtr; *sptr && *sptr != ']'; sptr++)
         {
            if (*(unsigned short*)sptr == '[]') sptr++;
            if (*(unsigned short*)sptr == '::') break;
         }
         Length = sptr - TagNameEndCharPtr;
         if (*(unsigned short*)sptr == '::')
            for (/* continue */; *sptr && *sptr != ']'; sptr++)
               if (*(unsigned short*)sptr == '[]') sptr++;
      } 
      else
      {
         for (sptr = TagNameEndCharPtr; *sptr && *sptr != ']'; sptr++)
         {
            if (*(unsigned short*)sptr == '[]') sptr++;
            if (*(unsigned short*)sptr == '::') { sptr += 2; break; }
         }
         for (TagNameEndCharPtr = sptr; *sptr && *sptr != ']'; sptr++)
            if (*(unsigned short*)sptr == '[]') sptr++;
         Length = sptr - TagNameEndCharPtr;
      }
   }
   else
   if (*TagNameEndCharPtr == '?')
   {
      TagNameEndCharPtr++;
      if (TagValue != NULL && strsame (TagValue, "ON", -1))
      {
         for (sptr = TagNameEndCharPtr; *sptr && *sptr != ']'; sptr++)
         {
            if (*(unsigned short*)sptr == '[]') sptr++;
            if (*(unsigned short*)sptr == '\?\?') break;
         }
         Length = sptr - TagNameEndCharPtr;
         if (*(unsigned short*)sptr == '\?\?')
            for (/* continue */; *sptr && *sptr != ']'; sptr++)
               if (*(unsigned short*)sptr == '[]') sptr++;
      }
      else
      {
         for (sptr = TagNameEndCharPtr; *sptr && *sptr != ']'; sptr++)
         {
            if (*(unsigned short*)sptr == '[]') sptr++;
            if (*(unsigned short*)sptr == '\?\?') { sptr += 2; break; }
         }
         for (TagNameEndCharPtr = sptr; *sptr && *sptr != ']'; sptr++)
            if (*(unsigned short*)sptr == '[]') sptr++;
         Length = sptr - TagNameEndCharPtr;
      }
   }
   else
      return (TagValue);

   if (Debug) fprintf (stdout, "Length: %d\n", Length);
   if (!Length) return ("");

   if ((cptr = vptr = calloc (Length+1, 1)) == NULL)
   {
      CgiLibResponseError (FI_LI, vaxc$errno, "calloc()");
      exit (SS$_NORMAL);
   }
   for (sptr = TagNameEndCharPtr; *sptr && Length; *cptr++ = *sptr++)
   {
      if (*(unsigned short*)sptr == '[[' ||
          *(unsigned short*)sptr == '[:' ||
          *(unsigned short*)sptr == '[?' ||
          *(unsigned short*)sptr == '[]')
      {
         if (!Length) break;
         Length--;
         sptr++;
      }
      if (!Length) break;
      Length--;
   }
   *cptr = '\0';
   return (vptr);
}

/*****************************************************************************/
/*
Use the VMS callable mail interface to create and send a VMS mail message.  
'To' can be  a list of comma-separated addresses.  'Subject' is a
null-terminated string. 'Body' is a  null-terminated string of '\n'-separated
lines of plain text.  Just truncates anything longer than 255 characters (body
excluded, body records included)!
*/ 

int MailMessage
(
char *PersonalName,
char *To,
char *Subject,
char *Body
)
{
   int  status;
   unsigned long  SendContext = 0;
   char  *cptr, *sptr;

   struct {
      short int  buf_len;
      short int  item;
      void  *buf_addr;
      unsigned short  *ret_len;
   }
   BodyPartItem [] =
   {
      { 0, MAIL$_SEND_RECORD, 0, 0 },
      { 0, MAIL$_NOSIGNAL, 0, 0 },
      {0,0,0,0}
   },
   PersonalNameItem [] =
   {
      { 0, MAIL$_SEND_PERS_NAME, PersonalName, 0 },
      { 0, MAIL$_NOSIGNAL, 0, 0 },
      {0,0,0,0}
   },
   SendUserNameItem [] =
   {
      { 0, MAIL$_SEND_USERNAME, 0, 0 },
      { 0, MAIL$_NOSIGNAL, 0, 0 },
      {0,0,0,0}
   },
   SubjectItem [] =
   {
      { 0, MAIL$_SEND_SUBJECT, Subject, 0 },
      { 0, MAIL$_NOSIGNAL, 0, 0 },
      {0,0,0,0}
   },
   NoSignalItem [] =
   {
      { 0, MAIL$_NOSIGNAL, 0, 0 },
      {0,0,0,0}
   },
   NullItem = {0,0,0,0};

   /*********/
   /* begin */
   /*********/

   if (Debug)
     fprintf (stdout, "MailMessage() |%s|\n|%s|\n|%s|\n",
              PersonalName, To, Subject);

   if (PersonalName != NULL && PersonalName[0])
   {
      PersonalNameItem[0].buf_len = strlen(PersonalName);
      if (PersonalNameItem[0].buf_len > 255) PersonalNameItem[0].buf_len = 255;
      status = mail$send_begin (&SendContext, &PersonalNameItem, &NullItem);
   }
   else
      status = mail$send_begin (&SendContext, &NoSignalItem, &NullItem);

   if (VMSnok (status))
   {
      CgiLibResponseError (FI_LI, status, "beginning message");
      exit (SS$_NORMAL);
   }

   /* a single, or multiple comma-separated addresses */
   cptr = To;
   while (*cptr)
   {
      sptr = cptr;
      while (*cptr && *cptr != ',') cptr++;
      if (*cptr) *cptr++ = '\0';

      SendUserNameItem[0].buf_addr = sptr;
      SendUserNameItem[0].buf_len = strlen(sptr);
      if (SendUserNameItem[0].buf_len > 255) SendUserNameItem[0].buf_len = 255;

      if (Debug)
         fprintf (stdout, "address |%s|\n",
                 (char*)SendUserNameItem[0].buf_addr);
      status = mail$send_add_address (&SendContext, &SendUserNameItem,
                                      &NullItem);
      if (VMSnok (status))
      {
         CgiLibResponseError (FI_LI, status, sptr);
         exit (SS$_NORMAL);
      }
   }

   SubjectItem[0].buf_len = strlen(Subject);
   if (SubjectItem[0].buf_len > 255) SubjectItem[0].buf_len = 255;
   status = mail$send_add_attribute (&SendContext, &SubjectItem, &NullItem);
   if (VMSnok (status))
   {
      CgiLibResponseError (FI_LI, status, "adding subject");
      exit (SS$_NORMAL);
   }

   cptr = Body;
   while (*cptr)
   {
      BodyPartItem[0].buf_addr = sptr = cptr;
      /* break at 255 characters regardless */
      while (*cptr && cptr-sptr < 255 && *cptr != '\n') cptr++;
      BodyPartItem[0].buf_len = cptr - sptr;
      if (*cptr && *cptr == '\n') cptr++;
      status = mail$send_add_bodypart (&SendContext, &BodyPartItem, &NullItem);
      if (VMSnok (status))
      {
         CgiLibResponseError (FI_LI, status, "adding body");
         exit (SS$_NORMAL);
      }
   }

   status = mail$send_message (&SendContext, &NoSignalItem, &NoSignalItem);
   if (VMSnok (status))
   {
       CgiLibResponseError (FI_LI, status, "sending message");
       exit (SS$_NORMAL);
   }

   mail$send_end (&SendContext, &NullItem, &NullItem);

   return (SS$_NORMAL);
}

/*****************************************************************************/
/*
Just reveal the contents of the mail message (for template debugging).
*/ 

RevealMailMessage
(
char *PersonalName,
char *To,
char *Subject,
char *Body
)
{
   /*********/
   /* begin */
   /*********/

   if (Debug) fprintf (stdout, "RevealMailMessage()\n");

   fprintf (stdout,
"From:   HTTP$SERVER \"%s\"\n\
To:     %s\n\
CC:\n\
Subj:   %s\n\
\n\
%s",
            PersonalName, To+1, Subject, Body);
}

/****************************************************************************/
/*
Does a case-insensitive, character-by-character string compare and returns
true if two strings are the same, or false if not.
*/

boolean strsame
(
char *sptr1,
char *sptr2,
int  count
)
{
   /*********/
   /* begin */
   /*********/

   if (count > 0)
      return (strncasecmp (sptr1, sptr2, count) == 0);
   else
      return (strcasecmp (sptr1, sptr2) == 0);
}

/*****************************************************************************/

