/*****************************************************************************/
/*
                                 SendMail.c

Use VMS callable Mail to send a VMS Mail message (no MIME, etc.)


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
---------------
06-JUL-2006  MGD  SendMailAddressExtract() comma-separated processing
06-MAY-2006  MGD  SendMailAddressExtract() allow blackslash (0x92) to escape
                    a single character (at least inside a quoted string)
01-FEB-2005  MGD  initial
*/
/*****************************************************************************/

#pragma nomember_alignment

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

/* VMS related header files */
#include <descrip.h>
#include <lnmdef.h>
#include <maildef.h>
#include <mailmsgdef.h>
#include <mail$routines.h>
#include <prvdef.h>
#include <psldef.h>
#include <ssdef.h>
#include <stsdef.h>

/* application header file */
#include "soymail.h"
#include "other.h"
#include "mta.h"
#include "sendmail.h"

#define FI_LI "SENDMAIL", __LINE__

/* external storage */

extern BOOL  Debug,
             WatchEnabled;

extern int  VmsVersion;

/* required prototypes */
int sys$fao (__unknown_params);
int sys$getmsg (__unknown_params);
int sys$gettim (__unknown_params);
int sys$setprv (__unknown_params);

/*****************************************************************************/
/*
Use callable Mail to send a VMS Mail message (no MIME, etc.)
*/

char* SendMailMessage
(
char *MailFrom,
char *ToList,
char *CcList,
char *MailSubj,
char *MailText
) 
{
   static unsigned long  SysPrvMask [2] = { PRV$M_SYSPRV, 0 };
   static unsigned short  UserNameTypeTo = MAIL$_TO,
                          UserNameTypeCc = MAIL$_CC;
   static unsigned short  Length;
   static char ErrorString [256];

   static VMS_ITEM_LIST3

   BodyPartItem [] =
   {
      { 0, MAIL$_SEND_RECORD, 0, 0 },
      { 0, MAIL$_NOSIGNAL, 0, 0 },
      {0,0,0,0}
   },
   FromItem [] =
   {
      { 0, MAIL$_SEND_FROM_LINE, 0, 0 },
      { 0, MAIL$_NOSIGNAL, 0, 0 },
      {0,0,0,0}
   },
   SendBeginSigFileItem [] =
   {
      { 0, MAIL$_SEND_NO_SIGFILE, 0, 0 },
      { 0, MAIL$_SEND_NO_PERS_NAME, 0, 0 },
      { 0, MAIL$_NOSIGNAL, 0, 0 },
      {0,0,0,0}
   },
   SendBeginNoSigFileItem [] =
   {
      { 0, MAIL$_SEND_NO_PERS_NAME, 0, 0 },
      { 0, MAIL$_NOSIGNAL, 0, 0 },
      {0,0,0,0}
   },
   SendToItem [] =
   {
      { 0, MAIL$_SEND_USERNAME, 0, 0 },
      { sizeof(UserNameTypeTo), MAIL$_SEND_USERNAME_TYPE, &UserNameTypeTo, 0 },
      { 0, MAIL$_NOSIGNAL, 0, 0 },
      {0,0,0,0}
   },
   SendCcItem [] =
   {
      { 0, MAIL$_SEND_USERNAME, 0, 0 },
      { sizeof(UserNameTypeCc), MAIL$_SEND_USERNAME_TYPE, &UserNameTypeCc, 0 },
      { 0, MAIL$_NOSIGNAL, 0, 0 },
      {0,0,0,0}
   },
   SubjectItem [] =
   {
      { 0, MAIL$_SEND_SUBJECT, 0, 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};

   int  cnt, status,
        VmsMailStatus,
        WrapAt;
   unsigned short  slen;
   unsigned long  SendContext;
   unsigned long  PrevPrivMask [2];
   char  *cptr, *sptr, *zptr;
   char  StringBuffer [256];
   $DESCRIPTOR (ErrorStringDsc, ErrorString);
   $DESCRIPTOR (MailAddressDsc, "");
   $DESCRIPTOR (StringBufferDsc, StringBuffer);

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

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

   /* let's keep it within the bounds of reason! */
   WrapAt = SOY_WRAP_AT;
   if (WrapAt && (WrapAt < 48 || WrapAt > 255)) WrapAt = SOY_WRAP_AT;

   SendContext = 0;

   /* basically to support pre-VMS6.2 where SEND_NO_SIGFILE is an error */
   if (VmsVersion >= 70)
   {
      status = mail$send_begin (&SendContext,
                                &SendBeginSigFileItem,
                                &NullItem);
      if (Debug) fprintf (stdout, "mail$send_begin() %%X%08.08X\n", status);
   }
   else
   {
      status = mail$send_begin (&SendContext,
                                &SendBeginNoSigFileItem,
                                &NullItem);
      if (Debug) fprintf (stdout, "mail$send_begin() %%X%08.08X\n", status);
   }
   if (VMSnok (status)) goto VmsEmailMessageError;

   /* callable Mail "From:" (apparently) must be set after send_begin() */
   if (MailFrom && MailFrom[0])
   {
      if (WatchEnabled) WatchThis (FI_LI, "FROM {!UL}!AZ", strlen(MailFrom), MailFrom);
      status = sys$setprv (1, &SysPrvMask, 0, &PrevPrivMask);
      if (Debug) fprintf (stdout, "sys$setprv() %%X%08.08X\n", status);
      FromItem[0].buf_addr = MailFrom;
      FromItem[0].buf_len = strlen(MailFrom);
      if (FromItem[0].buf_len > 255) FromItem[0].buf_len = 255;
      status = mail$send_add_attribute (&SendContext, &FromItem, &NullItem);
      if (!(PrevPrivMask[0] & PRV$M_SYSPRV)) sys$setprv (0, &SysPrvMask, 0, 0);
      if (Debug) fprintf (stdout, "$send_add_attribute() %%X%08.08X\n", status);
      if (VMSnok (status)) goto VmsEmailMessageError;
   }

   /******************/
   /* to address(es) */
   /******************/

   cptr = ToList;
   while (cptr && *cptr)
   {
      sptr = SendMailAddressExtract (&cptr);
      /* continue if no address could be parsed */
      if (!sptr || !*sptr) continue;
      if (WatchEnabled) WatchThis (FI_LI, "TO {!UL}!AZ", strlen(sptr), sptr);
      SendToItem[0].buf_addr = sptr;
      SendToItem[0].buf_len = strlen(sptr);
      if (SendToItem[0].buf_len > 255) SendToItem[0].buf_len = 255;
      status = mail$send_add_address (&SendContext, &SendToItem, &NullItem);
      if (Debug) fprintf (stdout, "$send_add_address() %%X%08.08X\n", status);
      if (VMSnok (status))
      {
         MailAddressDsc.dsc$a_pointer = SendToItem[0].buf_addr;
         MailAddressDsc.dsc$w_length = SendToItem[0].buf_len;
         goto VmsEmailMessageError;
      }
   }

   /******************/
   /* cc address(es) */
   /******************/

   cptr = CcList;
   while (cptr && *cptr)
   {
      sptr = SendMailAddressExtract (&cptr);
      /* continue if no address could be parsed */
      if (!sptr || !*sptr) continue;
      if (WatchEnabled) WatchThis (FI_LI, "CC {!UL}!AZ", strlen(sptr), sptr);
      SendCcItem[0].buf_addr = sptr;
      SendCcItem[0].buf_len = strlen(sptr);
      if (SendCcItem[0].buf_len > 255) SendCcItem[0].buf_len = 255;
      status = mail$send_add_address (&SendContext, &SendCcItem, &NullItem);
      if (Debug) fprintf (stdout, "$send_add_address() %%X%08.08X\n", status);
      if (VMSnok (status))
      {
         MailAddressDsc.dsc$a_pointer = SendCcItem[0].buf_addr;
         MailAddressDsc.dsc$w_length = SendCcItem[0].buf_len;
         goto VmsEmailMessageError;
      }
   }

   /***********/
   /* subject */
   /***********/

   if (MailSubj && MailSubj[0])
   {
      if (WatchEnabled) WatchThis (FI_LI, "SUBJ {!UL}!AZ", strlen(MailSubj), MailSubj);
      SubjectItem[0].buf_addr = MailSubj;
      SubjectItem[0].buf_len = strlen(MailSubj);
      if (SubjectItem[0].buf_len > 255) SubjectItem[0].buf_len = 255;
      status = mail$send_add_attribute (&SendContext, &SubjectItem, &NullItem);
      if (Debug) fprintf (stdout, "$send_add_attribute() %%X%08.08X\n", status);
      if (VMSnok (status)) goto VmsEmailMessageError;
   }

   /********/
   /* body */
   /********/

   cptr = MailText;
   while (cptr && *cptr)
   {
      BodyPartItem[0].buf_addr = cptr;

      /* only wrap non-quoted lines (those without leading '>') */
      if (WrapAt && *cptr != '>')
         cnt = WrapAt;
      else
         cnt = 255;

      while (*cptr && *cptr != '\r' && *cptr != '\n' && cnt)
      {
         cptr++;
         cnt--;
      }
      if (!cnt && *cptr != ' ' && *cptr != '\t')
      {
         /* reached the "wrap-at" limit while in a "word", back a little */
         sptr = cptr;
         while (*cptr && *cptr != ' ' && *cptr != '\t' &&
                cptr > (char*)BodyPartItem[0].buf_addr) cptr--;
         /* if it's a continuous line of characters then just break it! */
         if (cptr == BodyPartItem[0].buf_addr) cptr = sptr;
      }

      BodyPartItem[0].buf_len = cptr - (char*)BodyPartItem[0].buf_addr;
      if (Debug)
         fprintf (stdout, " %d |%*.*s|\n",
                  BodyPartItem[0].buf_len, BodyPartItem[0].buf_len,
                  BodyPartItem[0].buf_len, (char*)BodyPartItem[0].buf_addr);

      status = mail$send_add_bodypart (&SendContext, &BodyPartItem, &NullItem);
      if (Debug) fprintf (stdout, "$send_add_bodypart() %%X%08.08X\n", status);

      if (VMSnok (status)) break;

      /* if it was wrapped then this will absorb leading white-space */
      while (*cptr && (*cptr == ' ' || *cptr == '\t')) cptr++;
      if (*cptr == '\r') sptr = ++cptr;
      if (*cptr == '\n') sptr = ++cptr;
   }

   /***************/
   /* end message */
   /***************/

   status = mail$send_message (&SendContext, &NoSignalItem, &NoSignalItem);
   if (Debug) fprintf (stdout, "mail$send_message() %%X%08.08X\n", status);

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

   return (NULL);

   VmsEmailMessageError:

   VmsMailStatus = status;

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

   StringBufferDsc.dsc$w_length = sizeof(StringBuffer)-2;
   status = sys$getmsg (VmsMailStatus, &slen, &StringBufferDsc, 1, 0);
   if (VMSok (status))
   {
      StringBuffer[slen++] = '.';
      StringBuffer[slen] = '\0';
      StringBuffer[0] = toupper(StringBuffer[0]);
      if (Debug) fprintf (stdout, "|%s|\n", StringBuffer);
   }
   else
   {
      sprintf (ErrorString, "SYS$GETMSG() error! %%X%08.08X", status);
      if (WatchEnabled) WatchThis (FI_LI, "ERROR !AZ", ErrorString);
      return (ErrorString);
   }

   if (VmsMailStatus == MAIL$_LOGLINK ||
       VmsMailStatus == MAIL$_NOSUCHUSR ||
       VmsMailStatus == MAIL$_PARSEFAIL ||
       VmsMailStatus == MAIL$_USERSPEC ||
       VmsMailStatus == MAIL$_ERRACTRNS)
   {
      StringBufferDsc.dsc$w_length = slen;
      status = sys$fao (&StringBufferDsc, &slen, &ErrorStringDsc,
                        &MailAddressDsc);
      if (VMSok (status))
         ErrorString[slen] = '\0';
      else
         sprintf (ErrorString, "SYS$FAO() error! %%X%08.08X", status);
   }
   else
      strcpy (ErrorString, StringBuffer);

   if (WatchEnabled) WatchThis (FI_LI, "ERROR !AZ", ErrorString);

   return (ErrorString);
}

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

No need to explicitly constrain the header strings to 255 characters because if
they are unacceptable callable Mail would have returned an error already to we
know that they are of acceptable length!
*/ 

int SendMailToFile
(
char *FileName,
char *FromPtr,
char *ToPtr,
char *CcPtr,
char *SubjPtr,
char *BodyPtr
)
{
   static $DESCRIPTOR (DateTimeFaoDsc, "!%D\0");

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

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

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

   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");
   /* 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", ToPtr);
   fprintf (fp, "CC:\t%s\n", CcPtr);
   retval = fprintf (fp, "Subj:\t%s\n\n", SubjPtr);

   cptr = BodyPtr;
   while (*cptr)
   {
      for (sptr = cptr; *sptr && *sptr != '\r' && *sptr != '\n'; sptr++);
      retval = fwrite (cptr, sptr-cptr, 1, fp);
      if (*sptr == '\r') sptr++;
      if (*sptr == '\n') sptr++;
      cptr = sptr;
      if (retval <= 0) break;
   }

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

   fclose (fp);

   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* SendMailAddressExtract (char **ListPtrPtr)

{
   static char  abuf [256];

   char  *cptr, *sptr, *zptr;

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

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

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

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

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

      while (*cptr && isspace(*cptr) && *cptr != ',' && *cptr != '\n') cptr++;
      if (!*cptr) break;
      if (*cptr == '\"')
      {
         cptr++;
         while (*cptr && *cptr != '\"' && *cptr != '\n')
         {
            if (*cptr == '\\' && *(cptr+1) && *(cptr+1) != '\n') cptr++;
            cptr++;
         }
         if (*cptr == '\"') cptr++;
         continue;
      }
      while (*cptr && !isspace(*cptr) && *cptr != ',' && sptr < zptr)
         *sptr++ = toupper(*cptr++);
      *sptr = '\0';

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

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

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

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

   return (abuf);
}

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

