/*****************************************************************************/
/*
                                    mta.c

An elementary, (semi-)standalone SMTP Message Transport Agent (MTA).

It uses the TCP/IP Services QIO interface.  It connects to the specified SMTP
server host ('HostName') at port 25 (defaults to LOCALHOST) and conducts an
RFC821/2821 dialog to originate a pre-formatted RFC822/2822 message.  The
destination addresses ('AddrTo') should be a null-terminated string containing
one or more newline-terminated RFC822/2822 addresses acceptble to the SMTP
server.  If the header text is NULL it will generate an elementary RFC822/2822
mail header.  The body text should be either a null-terminated string
('BodyText') or a linked list of such texts ('HeaderList' and 'BodyList', see
TEXT_DATA_STRUCT below), acceptable to an SMTP message agent.  It returns a
NULL if the message was given to the SMTP server without detected error, or a
pointer to static storage containing an error message (the most commonly the
response of the SMTP server).

This module is designed to be able to be plucked from it's current code context
and dropped into somewhere else.  It *should* be usable on all versions of VMS
at least from V6.0 and later.


COPYRIGHT
---------
Copyright (C) 2005-2025 Mark G.Daniel
This program, comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it under the
conditions of the GNU GENERAL PUBLIC LICENSE, version 3, or any later version.


VERSION HISTORY
---------------
08-JAN-2023  MGD  MtaMsgToFile() change in fwrite() behaviour with at least
                    VSI C V7.4-001 on OpenVMS IA64 V8.4-2L3
                    meaning fwrite("",0,1,fp) fails to write a record
09-JUL-2011  MGD  MtaMsgToFile() accomodate addresses containing hyphens
11-MAY-2008  MGD  MtaMsgSmtpToServer() allow but do not RCPT any address 
                    "(whatever)"@whatever (with BCC allows non-disclosure)
07-NOV-2007  MGD  MtaMsgSmtpToServer() improve error detection and reporting
15-APR-2007  MGD  MtaHostNameByAddr() convenience function
04-FEB-2007  MGD  bugfix; MtaAddressExtract() *cptr assign instead of equality
12-JUN-2006  MGD  modifications to MtaRfcBinDate() and MtaTimezone() to allow
                    binary time and timezone to be passed as parameters
06-MAY-2006  MGD  bugfix; AddressIsRfc() RFC2822 only allows the double
                    quotation character (0x34) to quote
                  bugfix; AddressIsRfc() and MtaAddressExtract() allow
                    backslash (0x92) to escape a single character 
                  bugfix; MtaMsgSmtpToServer() thanks Hunter Goatley
21-APR-2006  MGD  MtaMsgSmtpToServer() allow a port number to be specified
                    with the SMTP server host (i.e. "the.host.name:25")
21-MAR-2006  MGD  MtaSmtpServerPoll() to provide MTA details for watch
15-MAR-2006  MGD  use MtaAddressExtract() for SMTP "MAIL from:<..>" address
01-FEB-2005  MGD  initial
*/
/*****************************************************************************/

#ifdef SOYMAIL_VMS_V7
#undef _VMS_V6_SOURCE
#define _VMS_V6_SOURCE
#undef __VMS_VER
#define __VMS_VER 70000000
#undef __CRTL_VER
#define __CRTL_VER 70000000
#endif

/* standard C header files */
#include <ctype.h>
#include <errno.h>
#include <ints.h>
#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stat.h>

/* VMS related header files */
#include <descrip.h>
#include <dvidef.h>
#include <iodef.h>
#include <lib$routines.h>
#include <lnmdef.h>
#include <starlet.h>
#include <ssdef.h>
#include <stsdef.h>

/* Internet-related header files */
#include <socket.h>
#include <in.h>
#include <inet.h>

/* application header files */
#include "cgilib.h"

/**
HP C V7.1-015 on OpenVMS Alpha V8.3 reports the following (apparently) because
of a nesting error in RESOLV.H due to the __CRTL_VER constraint that WASD
applications are built with.  This does not happen with Compaq C V6.4-005 on
OpenVMS Alpha V7.3-2 for instance.  Just avoid the error by pretending the
header file is already loaded.  Doesn't *seem* to cause side-effects.

#pragma __member_alignment __restore
........^
%CC-W-ALIGNPOP, This "restore" has underflowed the member alignment's stack. No corresponding "save" was found.
at line number 451 in module RESOLV of text library SYS$COMMON:[SYSLIB]DECC$RTLD
**/

#define __RESOLV_LOADED 1
#include <netdb.h>

/* application specific */
#include "mta.h"

/* define required values from UCX$INETDEF.H (Multinet does not supply one) */
#define INET_PROTYP$C_STREAM 1
#define INETACP$C_TRANS 2
#define INETACP_FUNC$C_GETHOSTBYNAME 1
#define INETACP_FUNC$C_GETHOSTBYADDR 2
#define UCX$C_AF_INET 2
#define UCX$C_DSC_ALL 2
#define UCX$C_FULL_DUPLEX_CLOSE 8192
#define UCX$C_REUSEADDR 4
#define UCX$C_SOCK_NAME 4
#define UCX$C_SOCKOPT 1
#define UCX$C_TCP 6

#define RMS$_RNF 98994

#define VMSok(x) ((x) & STS$M_SUCCESS)
#define VMSnok(x) !(((x) & STS$M_SUCCESS))

#define FI_LI "MTA", __LINE__

struct AnIOsb {
   unsigned short  Status;
   unsigned short  Count;
   unsigned long  Unused;
}  IOsb;

$DESCRIPTOR (InetDeviceDsc, "UCX$DEVICE");

int  OptionEnabled = 1;

struct {
   unsigned short  Length;
   unsigned short  Parameter;
   void  *Address;
} ReuseAddress =
   { sizeof(OptionEnabled), UCX$C_REUSEADDR, &OptionEnabled },
  ReuseAddressSocketOption =
   { sizeof(ReuseAddress), UCX$C_SOCKOPT, &ReuseAddress };

unsigned short  TcpSocket [3] =
   { UCX$C_TCP, INET_PROTYP$C_STREAM, UCX$C_AF_INET };

/*************/
/* text list */
/*************/

typedef struct TEXT_DATA_STRUCT {
   void *NextPtr;
   char *TextPtr;
   int TextSize;
   int TextLength;
} TEXT_DATA;

#define TEXT_DATA_SIZEOF (sizeof(void*)+sizeof(char*)+sizeof(int)+sizeof(int))

#define TEXT_DATA_CALLOC(tlptr,size) \
tlptr = (struct TEXT_DATA_STRUCT*) CgiLibVeeMemCalloc (TEXT_DATA_SIZEOF+size); \
if (tlptr) tlptr->TextSize = size;

#define TEXT_DATA_NEXT(tlptr) (((struct TEXT_DATA_STRUCT*)tlptr)->NextPtr)
#define TEXT_DATA_TEXT(tlptr) (((struct TEXT_DATA_STRUCT*)tlptr)->TextPtr)
#define TEXT_DATA_SIZE(tlptr) (((struct TEXT_DATA_STRUCT*)tlptr)->TextSize)
#define TEXT_DATA_LENGTH(tlptr) (((struct TEXT_DATA_STRUCT*)tlptr)->TextLength)

/* if the text follows immediately on from the structure */
#define TEXT_DATA_SELF(tlptr) ((char*)(tlptr+TEXT_DATA_SIZEOF))

/********************/
/* external storage */
/********************/

void WatchThis (char*, int, char*, ...);
int WatchDump (char*, int);

extern int  Debug,
            WatchEnabled;

int ErrorExit (int, char*, int);

/*****************************************************************************/
/*
An elementary, standalone SMTP Message Transport Agent (MTA).
*/ 

char* MtaMsgSmtpToServer
(
char *HostName,
char *AddrFrom,
char *AddrTo,
char *HeaderText,
void *HeaderList,
char *BodyText,
void *BodyList
)
{
   static char  ErrorBuffer [1024+1],
                ReadBuffer [1024+1];

   int  len, status,
        AddressCount,
        HeaderLength,
        ReplyCode,
        SectionCount,
        SmtpPort = 25,
        InsideData = 0;
   unsigned short  SmtpChannel,
                   TwoCharBuffer;
   unsigned long  BinTime [2];
   char  *aptr, *cptr, *sptr, *zptr;
   char  LocalHostName [128],
         SmtpHostName [128],
         WriteBuffer [1024+1];
   TEXT_DATA  *tlptr;
   TEXT_DATA  DummyTextList;

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

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

   if (gethostname (LocalHostName, sizeof(LocalHostName)))
      strcpy (LocalHostName, "localhost");

   if (!HostName) HostName = LocalHostName;

   zptr = (sptr = SmtpHostName) + sizeof(SmtpHostName)-1;
   for (cptr = HostName;
        *cptr && *cptr != ':' && sptr < zptr;
        *sptr++ = *cptr++);
   *sptr = '\0';
   if (*cptr)
   {
      cptr++;
      if (isdigit(*cptr)) SmtpPort = atoi(cptr);
   }

   status = MtaOpenNet (&SmtpChannel, SmtpHostName, SmtpPort, NULL);
   if (VMSnok (status))
   {
      sprintf (ReadBuffer, "%s", MtaSysGetMsg(status));
      return (ReadBuffer);
   }

   ReplyCode = MtaWriteSmtpRead (SmtpChannel, NULL, 0,
                                 ReadBuffer, sizeof(ReadBuffer), 1);
   if (ReplyCode != 220)
   {
      sys$dassgn (SmtpChannel);
      return (ReadBuffer);
   }

   len = sprintf (WriteBuffer, "HELO %s\r\n", HostName);
   ReplyCode = MtaWriteSmtpRead (SmtpChannel, WriteBuffer, len,
                                 ReadBuffer, sizeof(ReadBuffer), 1);
   if (ReplyCode != 250)
   {
      MtaWriteSmtpRead (SmtpChannel, "QUIT\r\n", -1, NULL, 0, 0);
      sys$dassgn (SmtpChannel);
      return (ReadBuffer);
   }

   cptr = AddrFrom;
   sptr = MtaAddressExtract (&cptr);
   len = sprintf (WriteBuffer, "MAIL from:<%s>\r\n", sptr);
   ReplyCode = MtaWriteSmtpRead (SmtpChannel, WriteBuffer, len,
                                 ReadBuffer, sizeof(ReadBuffer), 1);
   if (ReplyCode != 250)
   {
      MtaWriteSmtpRead (SmtpChannel, "QUIT\r\n", -1, NULL, 0, 0);
      sys$dassgn (SmtpChannel);
      for (zptr = ReadBuffer; *zptr && *zptr != '\r' && *zptr != '\n'; zptr++);
      *zptr = '\0';
      sprintf (ErrorBuffer, "%s: %s", ReadBuffer, AddrFrom);
      return (ErrorBuffer);
   }

   cptr = AddrTo;
   while (*cptr)
   {
      sptr = MtaAddressExtract (&cptr);
      /* continue if no address could be parsed */
      if (!sptr || !*sptr) continue;
      /* if an "(undisclosed)" address just continue */
      if (*(USHORTPTR)sptr == '\"(')
      {
         for (aptr = sptr+2; *aptr && *aptr != ')'; aptr++);
         if (*(USHORTPTR)aptr == ')\"') continue;
      }
      len = sprintf (WriteBuffer, "RCPT to:<%s>\r\n", sptr); 
      ReplyCode = MtaWriteSmtpRead (SmtpChannel, WriteBuffer, len,
                                    ReadBuffer, sizeof(ReadBuffer), 1);
      if (ReplyCode != 250)
      {
         MtaWriteSmtpRead (SmtpChannel, "QUIT\r\n", -1, NULL, 0, 0);
         sys$dassgn (SmtpChannel);
         for (zptr = ReadBuffer; *zptr && *zptr != '\r' && *zptr != '\n'; zptr++);
         *zptr = '\0';
         sprintf (ErrorBuffer, "%s: %s", ReadBuffer, sptr);
         return (ErrorBuffer);
      }
   }

   ReplyCode = MtaWriteSmtpRead (SmtpChannel, "DATA\r\n", 6,
                                 ReadBuffer, sizeof(ReadBuffer), 1);
   if (ReplyCode != 354)
   {
      MtaWriteSmtpRead (SmtpChannel, "QUIT\r\n", -1, NULL, 0, 0);
      sys$dassgn (SmtpChannel);
      return (ReadBuffer);
   }

   if (!HeaderList)
   {
      if (!HeaderText || !strchr(HeaderText, '\n'))
      {
         /* construct a primitive RFC822 header */
         sys$gettim (&BinTime);

         /* create a minimal RFC822 header for the message */
         len = sprintf (WriteBuffer,
"Message-ID: <%08.08X%08.08X@%s>\r\n\
Date: %s\r\n\
Subject: %s\r\n\
From: %s\r\n\
To: ",
                        BinTime[0], BinTime[1], LocalHostName,
                        MtaRfcBinDate(NULL,NULL),
                        HeaderText ? HeaderText : "(none)",
                        AddrFrom);

         zptr = (sptr = WriteBuffer) + sizeof(WriteBuffer) - len - 1;
         sptr += len;

         AddressCount = 0;
         cptr = AddrTo;
         while (*cptr)
         {
            aptr = cptr;
            /* now find the start of the/any next line */
            while (*cptr && *cptr != '\r' && *cptr != '\n') cptr++;
            if (aptr < cptr)
            {
               if (AddressCount++ && sptr < zptr) *sptr++ = ' ';
               while (aptr < cptr && sptr < zptr) *sptr++ = *aptr++;
               if (sptr < zptr) *sptr++ = '\r';
               if (sptr < zptr) *sptr++ = '\n';
            }
            while (*cptr && (*cptr == '\r' || *cptr == '\n')) cptr++;
         }
         if (sptr < zptr) *sptr++ = '\r';
         if (sptr < zptr) *sptr++ = '\n';
         *sptr = '\0';
         HeaderText = WriteBuffer;
         HeaderLength = sptr - HeaderText;
         if (Debug) fprintf (stdout, "%d |%s|\n", HeaderLength, HeaderText);
      }
      else
         HeaderLength = strlen(HeaderText);
   }

   if (!(tlptr = HeaderList))
   {
      /* fake a body text list */
      tlptr = &DummyTextList;
      TEXT_DATA_TEXT(tlptr) = HeaderText;
      TEXT_DATA_SIZE(tlptr) = TEXT_DATA_LENGTH(tlptr) = HeaderLength;
      TEXT_DATA_NEXT(tlptr) = NULL;
   }

   while (tlptr)
   {
      cptr = TEXT_DATA_TEXT(tlptr);
      len = TEXT_DATA_LENGTH(tlptr);
      tlptr = TEXT_DATA_NEXT(tlptr);

      if (!cptr) continue;

      ReplyCode = MtaWriteSmtpRead (SmtpChannel, cptr, len, NULL, 0, 0);
      if (ReplyCode & 0x8000000)
      {
         /* high bit set indicates a VMS status (bit kludgy I'll admit) */
         sys$dassgn (SmtpChannel);
         sprintf (ReadBuffer, "%s", MtaSysGetMsg(ReplyCode & 0x7fffffff));
         return (ReadBuffer);
      }
   }

   if (!(tlptr = BodyList))
   {
      /* fake a body text list */
      tlptr = &DummyTextList;
      TEXT_DATA_TEXT(tlptr) = BodyText ? BodyText : "";
      TEXT_DATA_SIZE(tlptr) = TEXT_DATA_LENGTH(tlptr) =
         strlen(TEXT_DATA_TEXT(tlptr));
      TEXT_DATA_NEXT(tlptr) = NULL;
   }

   while (tlptr)
   {
      cptr = TEXT_DATA_TEXT(tlptr);
      tlptr = TEXT_DATA_NEXT(tlptr);

      if (!cptr) continue;

      while (*cptr)
      {
         for (sptr = cptr; *sptr && *sptr != '\r' && *sptr != '\n'; sptr++);

         TwoCharBuffer = *(USHORTPTR)sptr;
         *(USHORTPTR)sptr = '\r\n';

         if (*cptr == '.')
         {
            if (sptr-cptr+2 > sizeof(WriteBuffer)-1)
               ErrorExit (SS$_BUGCHECK, FI_LI);
            WriteBuffer[0] = '.';
            memcpy (WriteBuffer+1, cptr, sptr-cptr+2);
            ReplyCode = MtaWriteSmtpRead (SmtpChannel, WriteBuffer, 
                                          sptr-cptr+3, NULL, 0, 0);
         }
         else
            ReplyCode = MtaWriteSmtpRead (SmtpChannel, cptr,
                                          sptr-cptr+2, NULL, 0, 0);

         if (ReplyCode & 0x8000000)
         {
            /* high bit set indicates a VMS status (bit kludgy I'll admit) */
            sys$dassgn (SmtpChannel);
            sprintf (ReadBuffer, "%s", MtaSysGetMsg(ReplyCode & 0x7fffffff));
            return (ReadBuffer);
         }

         *(USHORTPTR)sptr = TwoCharBuffer;

         cptr = sptr;
         if (*cptr == '\r') cptr++;
         if (*cptr == '\n') cptr++;
      }
   }

   ReplyCode = MtaWriteSmtpRead (SmtpChannel, ".\r\n", 3,
                                 ReadBuffer, sizeof(ReadBuffer), 1);

   MtaWriteSmtpRead (SmtpChannel, "QUIT\r\n", -1, NULL, 0, 0);
   sys$dassgn (SmtpChannel);

   if (ReplyCode != 250) return (ReadBuffer);

   return (NULL); 
}

/*****************************************************************************/
/*
Reproduce the same message content as in MtaMsgToSmtp() but into a file.
See CallMailCopyFile() for a description of the sequential file format.

Constrain the header strings used in the VMS format message to provide a
maximum of 255 characters otherwise CallMailCopyFile() will barf.

09-JUL-2011 Note: Christoph Gartmann reported a host name change from imunbio
to ie-freiburg resulted in the sent items folder stopping working and/or
strange dates appearing in the VMS Mail message header(s).  soyMAIL's "Sent
Items" facility works as described in CallMailCopyFile() and it appears that
the mail$message_copy() parse of the "From:" line confuses a hyphen in an
address with a date component!  Solution is to fudge a mail transport to
allowing quoting of the address.  This is only done when a hyphen is detected.
*/ 

int MtaMsgToFile
(
char *FileName,
char *FromPtr,
char *ToPtr,
char *CcPtr,
char *SubjPtr,
void *HeaderList,
void *BodyList
)
{
   static $DESCRIPTOR (DateTimeFaoDsc, "!%D\0");

   int  retval, status;
   int64  Time64;
   char  *cptr, *sptr, *zptr;
   char  Cc255 [256],
         DateTime [32],
         FromEsc [256],
         Subj255 [256],
         To255 [256];
   $DESCRIPTOR (DateTimeDsc, DateTime);
   FILE  *fp;
   TEXT_DATA  *tlptr;

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

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

   if (!HeaderList || !BodyList) ErrorExit (SS$_BUGCHECK, FI_LI);

   /* need to limit to a VMS Mail acceptable 255 */
   zptr = (sptr = To255) + sizeof(To255)-1;
   for (cptr = ToPtr; *cptr && *cptr != '\n' && sptr < zptr; *sptr++ = *cptr++);
   *sptr = '\0';

   /* need to limit to a VMS Mail acceptable 255 */
   zptr = (sptr = Cc255) + sizeof(Cc255)-1;
   for (cptr = CcPtr; *cptr && *cptr != '\n' && sptr < zptr; *sptr++ = *cptr++);
   *sptr = '\0';

   /* need to limit to a VMS Mail acceptable 255 */
   zptr = (sptr = Subj255) + sizeof(Subj255)-1;
   for (cptr = SubjPtr; *cptr && sptr < zptr; *sptr++ = *cptr++);
   *sptr = '\0';

   fp = fopen (FileName, "w", "rfm=var", "rat=cr", "mbc=32", "mbf=64");
   if (!fp) return (vaxc$errno);

   sys$gettim (&Time64);
   sys$fao (&DateTimeFaoDsc, 0, &DateTimeDsc, &Time64);

   /* NOTE THE LEADING FORM-FEED! (cost me a couple of hours) */
   fprintf (fp, "\f\n");
   /* hyphens are confusing (see note above) */
   if (strchr (FromPtr, '-'))
   {
      /* one for the null, one for the trailing quote, one elbow-room */
      zptr = (sptr = FromEsc) + sizeof(FromEsc)-3;
      for (cptr = "SOYMAIL%\""; *cptr && sptr < zptr; *sptr++ = *cptr++);
      /* escape existing quotes by doubling-up */
      for (cptr = FromPtr; *cptr && sptr < zptr; *sptr++ = *cptr++)
         if (*cptr == '\"') *sptr++ = '\"';
      *sptr++ = '\"';
      *sptr = '\0';
      FromPtr = FromEsc;
   }
   /* separate date/time using a null which tends to mask the date */
   fprintf (fp, "From:\t%s%c\t%s\n", FromPtr, 0, DateTime);
   fprintf (fp, "To:\t%s\n", To255);
   fprintf (fp, "CC:\t%s\n", Cc255);
   retval = fprintf (fp, "Subj:\t%s\n\n", Subj255);

   /* start with the header list ... */
   tlptr = HeaderList;
   while (tlptr)
   {
      cptr = TEXT_DATA_TEXT(tlptr);
      tlptr = TEXT_DATA_NEXT(tlptr);
      if (!tlptr)
      {
         /* ... and when that's exhausted continue with the body */
         tlptr = BodyList;
         BodyList = NULL;
      }
      if (!cptr) continue;
      while (*cptr)
      {
         for (sptr = cptr; *sptr && *sptr != '\r' && *sptr != '\n'; sptr++);
         if (sptr - cptr)
            retval = fwrite (cptr, sptr-cptr, 1, fp);
         else
            retval = fwrite ("\n", 1, 1, fp);
         if (*sptr == '\r') sptr++;
         if (*sptr == '\n') sptr++;
         cptr = sptr;
         if (retval <= 0) break;
      }
      if (retval <= 0) break;
   }

   if (retval <= 0)
      status = vaxc$errno;
   else
      status = SS$_NORMAL;

   fclose (fp);

   return (status); 
}

/*****************************************************************************/
/*
Connect to the specified SMTP server to get it's announcement string into the
watch output.
*/ 

void MtaSmtpServerPoll (char *HostName)

{
   int  status,
        SmtpPort = 25;
   unsigned short  SmtpChannel;
   char  *cptr, *sptr, *zptr;
   char  LocalHostName [128],
         SmtpHostName [128],
         ReadBuffer [255+1];

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

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

   if (gethostname (LocalHostName, sizeof(LocalHostName)))
      strcpy (LocalHostName, "localhost");

   if (!HostName) HostName = LocalHostName;

   zptr = (sptr = SmtpHostName) + sizeof(SmtpHostName)-1;
   for (cptr = HostName;
        *cptr && *cptr != ':' && sptr < zptr;
        *sptr++ = *cptr++);
   *sptr = '\0';
   if (*cptr)
   {
      cptr++;
      if (isdigit(*cptr)) SmtpPort = atoi(cptr);
   }

   if (WatchEnabled) WatchThis (FI_LI, "MTA poll ...");
   status = MtaOpenNet (&SmtpChannel, SmtpHostName, SmtpPort, NULL);
   if (VMSnok (status))
   {
      if (WatchEnabled) WatchThis (FI_LI, "%X!8XL !AZ", status, MtaSysGetMsg(status));
      return;
   }
   MtaWriteSmtpRead (SmtpChannel, NULL, 0, ReadBuffer, sizeof(ReadBuffer), 1);
   sys$qiow (0, SmtpChannel, IO$_WRITEVBLK, 0, 0, 0, "QUIT\r\n", 6, 0, 0, 0, 0);
   sys$dassgn (SmtpChannel);
}

/****************************************************************************/
/*
Does two things.  First, optionlly write a string to the SMTP server.  If
'DataPtr' is NULL this is not done.  Second, reads an optional reply line from
the server.  It returns the three digit SMTP status code as an integer.
*/

int MtaWriteSmtpRead
(
unsigned short SmtpChannel,
char *DataPtr,
int DataLength,
char *DataBuffer,
int SizeOfDataBuffer,
int ExpectReply
)
{
   int  status, clen,
        ReadCount;
   char  *cptr;
   char  ReadBuffer [512+1];

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

   if (Debug)
      fprintf (stdout, "MtaWriteSmtpRead() %d %*.*s\n",
               ExpectReply, DataLength, DataLength, DataPtr);

   if (DataPtr)
   {
      if (DataLength <= 0) DataLength = strlen(DataPtr);

      status = MtaWriteNet (SmtpChannel, DataPtr, DataLength);
      /* high bit set indicates a VMS status (bit kludgy I'll admit) */
      if (VMSnok (status)) return (status | 0x8000000);
   }

   if (!ExpectReply) return (200);

   if (DataBuffer)
   {
      cptr = DataBuffer;
      clen = SizeOfDataBuffer;
   }
   else
   {
      cptr = ReadBuffer;
      clen = sizeof(ReadBuffer);
   }
   status = MtaReadNet (SmtpChannel, cptr, clen, &ReadCount, 1);
   /* high bit set indicates a VMS status (bit kludgy I'll admit) */
   if (VMSnok (status))
   {
      cptr = MtaSysGetMsg (status);
      if (DataBuffer && strlen (cptr) < SizeOfDataBuffer)
         strcpy (DataBuffer, cptr);
      return (status | 0x8000000);
   }

   return (atoi(cptr));
}

/****************************************************************************/
/*
Open a "socket" connect to the specific host name and port.  If 'IpAddressPtr'
is not NULL then set where it points to the 32 IP address of the remote system.
*/

MtaOpenNet
(
unsigned short *ChannelPtr,
char *HostName,
int PortNumber,
int *IpAddressPtr
)
{
   int  status,
        IpAddress;
   unsigned short Channel;
   char  *cptr;
   struct AnIOsb  IOsb;
   struct sockaddr_in  SocketName;
   struct hostent  *HostEntryPtr;

   struct {
      unsigned long  Length;
      void  *Address;
      int  *LengthPtr;
   } SocketNameItem = { sizeof(SocketName), &SocketName, 0 };

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

   if (Debug) fprintf (stdout, "MtaOpenNet() %s:%d\n", HostName, PortNumber);

   if (WatchEnabled) WatchThis (FI_LI, "SMTP >> !AZ:!UL", HostName, PortNumber);

   if (isdigit((cptr = HostName)[0]))
      while (*cptr && (isdigit(*cptr) || *cptr == '.')) cptr++;
   if (!*cptr)
   {
      /*************************/
      /* address "131.185.2.4" */
      /*************************/

      if ((IpAddress = inet_addr (HostName)) == -1)
         return (vaxc$errno);
   }
   else
   {
      /***************************/
      /* address "the.host.name" */
      /***************************/

      if (!(HostEntryPtr = gethostbyname (HostName))) return (SS$_NOSUCHNODE);
      memcpy (&IpAddress, HostEntryPtr->h_addr, 4);
   }

   /* assign a channel to the internet template device */
   if (VMSnok (status = sys$assign (&InetDeviceDsc, &Channel, 0, 0)))
      return (status);

   /* make the channel a TCP, connection-oriented socket */
   status = sys$qiow (0, Channel, IO$_SETMODE, &IOsb, 0, 0,
                      &TcpSocket, 0, 0, 0, 0, 0);

   if (Debug)
      fprintf (stdout, "sys$qiow() %%X%08.08X IOsb.Status %%X%08.08X\n",
               status, IOsb.Status);
   if (VMSok (status) && VMSnok (IOsb.Status)) status = IOsb.Status;
   if (VMSnok (status)) return (status);

   SocketName.sin_family = AF_INET;
   SocketName.sin_port = htons (PortNumber);
   memcpy (&SocketName.sin_addr.s_addr, &IpAddress, 4);
   if (IpAddressPtr) memcpy (IpAddressPtr, &IpAddress, 4);

   status = sys$qiow (0, Channel, IO$_ACCESS, &IOsb, 0, 0,
                      0, 0, &SocketNameItem, 0, 0, 0);

   if (VMSok (status) && VMSnok (IOsb.Status)) status = IOsb.Status;
   if (VMSok (status))
      *ChannelPtr = Channel;
   else
   {
      *ChannelPtr = 0;
      if (WatchEnabled) WatchThis (FI_LI, "SMTP >> %X!8XL", status);
   }
   return (status);
}

/****************************************************************************/
/*
Read from the specific "socket" into the the supplied buffer.  If 'AsText' is
true the allow for and supply a terminating null character.  If 'ReadCountPtr'
is not NULL then set it's location to the number of bytes read.  If read
'AsText' the read buffer is adjusted to allow for a terminating null.
*/

int MtaReadNet
(
unsigned short Channel,
char *DataBuffer,
int SizeOfDataBuffer,
int *ReadCountPtr,
int AsText
)
{
   int  status;
   char  *cptr;
   struct AnIOsb  IOsb;

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

   if (Debug)
      fprintf (stdout, "MtaReadNet() %d %d\n", SizeOfDataBuffer, AsText);

   if (AsText) SizeOfDataBuffer--;

   status = sys$qiow (0, Channel, IO$_READVBLK, &IOsb, 0, 0,
                      DataBuffer, SizeOfDataBuffer, 0, 0, 0, 0);
   if (Debug)
      fprintf (stdout,
          "sys$qiow() status %%X%08.08X IOsb.Status %%X%08.08X %d bytes\n",
          status, IOsb.Status, IOsb.Count);

   if (VMSok (status) && VMSnok (IOsb.Status)) status = IOsb.Status;
   if (VMSok (status))
   {
      if (AsText)
      {
         char  *cptr, *sptr, *zptr;
         DataBuffer[IOsb.Count] = '\0';
         zptr = (cptr = DataBuffer) + IOsb.Count;
         while (cptr < zptr && *cptr != '\r' && *cptr != '\n') cptr++;
         sptr = cptr;
         if (cptr < zptr) cptr++;
         if (cptr < zptr) cptr++;
         if (WatchEnabled)
            WatchThis (FI_LI, "SMTP << {!UL-!UL}!#AZ",
                       IOsb.Count, cptr-sptr,
                       IOsb.Count-(cptr-sptr), DataBuffer);
      }

      if (ReadCountPtr) *ReadCountPtr = IOsb.Count;
   }
   else
   if (ReadCountPtr)
   {
      *ReadCountPtr = 0;
      if (WatchEnabled) WatchThis (FI_LI, "SMTP << %X!8XL", status);
   }

   return (status);
}

/****************************************************************************/
/*
Write the supplied data to the specific "socket".  If 'DataLength' is -1 then
consider the data to be a null-terminated string and get it's length using
strlen().
*/

int MtaWriteNet
(
unsigned short Channel,
char *DataPtr,
int DataLength
)
{
   int  status;
   struct AnIOsb  IOsb;

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

   if (Debug)
      fprintf (stdout, "MtaWriteNet() %d |%*.*s|\n",
               DataLength, DataLength, DataLength, DataPtr);

   if (DataLength == -1) DataLength = strlen(DataPtr);

   if (WatchEnabled)
   {
      char  *cptr, *sptr, *zptr;
      zptr = (cptr = DataPtr) + DataLength;
      while (cptr < zptr && *cptr != '\r' && *cptr != '\n') cptr++;
      sptr = cptr;
      if (cptr < zptr) cptr++;
      if (cptr < zptr) cptr++;
      WatchThis (FI_LI, "SMTP >> {!UL-!UL}!#AZ",
                 DataLength, cptr-sptr, DataLength-(cptr-sptr), DataPtr);
   }

   status = sys$qiow (0, Channel, IO$_WRITEVBLK, &IOsb, 0, 0,
                      DataPtr, DataLength, 0, 0, 0, 0);
   if (Debug)
      fprintf (stdout, "sys$qiow() %%X%08.08X IOsb.Status %%X%08.08X\n",
               status, IOsb.Status);

   if (VMSok (status) && VMSnok (IOsb.Status)) status = IOsb.Status;

   if (WatchEnabled) WatchThis (FI_LI, "SMTP >> %X!8XL", status);

   return (status);
}

/*****************************************************************************/
/*
An address list is a string containing one or more email addresses separated by
newline characters.  The newline character can optionally be preceded by a
comma (making it look like a comma-separated list).  Parse one of these
addresses from the list.  Return a pointer to a static buffer containing the
address, an empty string if end-of-list, or a NULL if an error was encountered. 
Update the supplied list pointer ready to parse the next address.
*/

char* MtaAddressExtract (char **ListPtrPtr)

{
   static char  abuf [256];

   char  *cptr, *lptr, *sptr, *zptr,
         *StartOfLinePtr;

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

   if (Debug) fprintf (stdout, "MtaAddressExtract() |%s|\n", *ListPtrPtr);

   if (!ListPtrPtr) return (NULL);
   if (!(lptr = *ListPtrPtr)) return (NULL);

   abuf[0] = '\0';
   while (*lptr)
   {
      zptr = (sptr = abuf) + sizeof(abuf);
      *sptr = '\0';

      while (*lptr && *lptr == '\n') lptr++;
      if (!*lptr) break;
      StartOfLinePtr = lptr;

      while (*lptr && *lptr != '\"' && *lptr != '@' && *lptr != '\n')
      {
         if (*lptr == '\\' && *(lptr+1) && *(lptr+1) != '\n') lptr++;
         lptr++;
      }
      if (!*lptr) break;
      if (*lptr == '\"')
      {
         cptr = lptr;
         lptr++;
         while (*lptr && *lptr != '\"' && *lptr != '\n')
         {
            if (*lptr == '\\' && *(lptr+1) && *(lptr+1) != '\n') lptr++;
            lptr++;
         }
         if (*lptr == '\"') lptr++;
         /* if it's not a quoted local part */
         if (*lptr && *lptr != '@' && *lptr != '\n') continue;
      }
      else
      {
         /* scan back to get the start of the local part */
         cptr = lptr;
         while (cptr > StartOfLinePtr)
         {
            if (isspace(*cptr)) {
               if (cptr > StartOfLinePtr) {
                  if (*(cptr-1) != '\\') break;
               }
               else
                  break;
            }
            cptr--;
         }
         while (*cptr && isspace(*cptr))
         {
            if (*cptr == '\\' && *(cptr+1) && *(cptr+1) != '\n') cptr++;
            cptr++;
         }
      }
      if (*cptr == '<') cptr++;
      while (cptr < lptr && sptr < zptr) *sptr++ = *cptr++;
      while (*lptr && !isspace(*lptr) && sptr < zptr) *sptr++ = *lptr++;
      /* allow for comma-separated list of addresses */
      if (sptr > abuf && *(sptr-1) == ',') sptr--;
      if (sptr > abuf && *(sptr-1) == '>') sptr--;
      if (sptr >= zptr)
      {
         abuf[0] = '!';
         sptr = abuf + sizeof(abuf)-1;
      }
      *sptr = '\0';

      /* scan to the end of this list line */
      while (*lptr && *lptr != '\n') lptr++;
      while (*lptr && *lptr == '\n') lptr++;

      /* if an empty line */
      if (!abuf[0]) continue;
      break;
   }

   if (WatchEnabled) WatchThis (FI_LI, "ADDRESS |!AZ", abuf);

   /* update the pointer to the list */
   *ListPtrPtr = lptr;

   return (abuf);
}

/*****************************************************************************/
/*
This function is included in MTA.C for pure convenience.  It just confines the
instances of the warning issue described immediately above the #include netdb.h
above.
*/

char* MtaHostNameByAddr (char *IpAddrStr)

{
   int  IpAddr;
   char  *cptr;
   struct hostent  *HostEntryPtr;

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

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

   IpAddr = inet_addr (IpAddrStr);
   if (IpAddr == -1) return (NULL);
   HostEntryPtr = gethostbyaddr (&IpAddr, 4, AF_INET);
   if (!HostEntryPtr) return (NULL);
   cptr =  CgiLibVeeMemCalloc (strlen(HostEntryPtr->h_name)+1);
   if (cptr) strcpy (cptr, HostEntryPtr->h_name);
   return (cptr); 
}

/*****************************************************************************/
/*
Return a pointer to an allocated string in the RFC/Unix-style format (e.g. "31
Mar 2005 20:35:16 +0930") for the supplied VMS binary time (current if NULL).
*/

char* MtaRfcBinDate
(
unsigned long *Time64Ptr,
char *TzDiffPtr
)
{
   static char  *DayName[] = { "", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
                                   "Sun" };
   static char  *MonName[] = { "", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
                                   "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };

   int  len;
   char  RfcDateString [64];
   char  *sptr;
   unsigned short  NumTime [7];
   unsigned long  DayOfWeek;

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

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

   sys$numtim (&NumTime, Time64Ptr);
   lib$day_of_week (Time64Ptr, &DayOfWeek);

   len = sprintf (RfcDateString, "%s, %02d %s %4d %02d:%02d:%02d %s",
                  DayName[DayOfWeek],
                  NumTime[2], MonName[NumTime[1]], NumTime[0], 
                  NumTime[3], NumTime[4], NumTime[5], MtaTimezone(TzDiffPtr));

   sptr =  CgiLibVeeMemCalloc (len+1);
   memcpy (sptr, RfcDateString, len);

   return (sptr);
}

/*****************************************************************************/
/*
The SYS$TIMEZONE_DIFFERENTIAL logical contains the number of seconds offset
from GMT (UTC) as a positive (ahead) or negative (behind) number.  Return a
pointer to a string containg "+hhmm" or "-hhmm" string equivalent.
*/

char* MtaTimezone (char *TzDiffPtr)

{
   static char  RfcTimezone [8],
                SysTimeDiff [8];
   static unsigned short  Length;
   static $DESCRIPTOR (TimezoneLogicalNameDsc, "SYS$TIMEZONE_DIFFERENTIAL");
   static $DESCRIPTOR (LnmSystemDsc, "LNM$SYSTEM");
   static struct {
      short int  buf_len;
      short int  item;
      void  *buf_addr;
      unsigned short  *ret_len;
   } LnmItems [] =
   {
      { sizeof(SysTimeDiff)-1, LNM$_STRING, SysTimeDiff, &Length },
      { 0,0,0,0 }
   };

   int  status;
   long  hh, mm, secs;
   char  *cptr;

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

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

   if (!TzDiffPtr)
   {
      status = sys$trnlnm (0, &LnmSystemDsc, &TimezoneLogicalNameDsc,
                           0, &LnmItems);
      if (VMSnok (status)) return ("-0000");
      SysTimeDiff[Length] = '\0';
      TzDiffPtr = SysTimeDiff;
   }

   secs = atol(TzDiffPtr);
   if (secs < 0)
   {
      secs = -secs;
      cptr = "-";
   }
   else
      cptr = "+";

   if (secs > 43200 || secs < -43200) return ("+0000");

   hh = secs / 3600;
   mm = (secs - hh * 3600) / 60;
   sprintf (RfcTimezone, "%s%02d%02d", cptr, hh, mm);
   return (RfcTimezone);
}

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

char* MtaSysGetMsg (int StatusValue)

{
   static char  StatusMsg [256];

   int  status;
   unsigned short  Length;
   char  *cptr;
   $DESCRIPTOR (StatusMsgDsc, StatusMsg);

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

   if (Debug) fprintf (stdout, "MtaSysGetMsg() %%X%08.08X\n", StatusValue);

   status = sys$getmsg (StatusValue, &Length, &StatusMsgDsc, 1, 0);
   if (!(status & 1)) exit (status);
   StatusMsg[Length] = '\0';
   StatusMsg[0] = toupper(StatusMsg[0]);
   if (cptr = strstr (StatusMsg, "!/!_")) *(ULONGPTR)cptr = '.   ';

   return (StatusMsg);
}

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

