/*****************************************************************************/
/*
                                  attach.c

Message part/attachment and it's associated file management.


ATTACHMENTS
-----------
soyMAIL manages attachments as files stored in the VMS Mail directory.  The
names of these files allow them to be distinguished from VMS Mail and other
other soyMAIL files.  They are named 'SOYMAIL_yyyymmddhhmmsshhnn.ATT' where
'yyyymmddhhmmsshh' are the numeric time components, and 'nn' a two-digit file
count.


UPLOAD
------
The message compose page allows files stored on the user's browser system to be
uploaded to the system running soyMAIL.  These files are listed on the compose
page and can be marked (checkboxed) for inclusion as MIME parts in the sent
message. 


SAVE
----
The message read page allows the MIME parts (usually attachments) of a received
message to be marked (checkboxed) and saved in the VMS Mail directory for
inclusion at the message composition page.

An attachment file has an internal structure.  The actual attachment content is
preceded by three fields with null-terminated strings explicitly providing the
soyMAIL version that saved them, the name and MIME content-type of the content.

  [soyMAIL-version]version\0
  [name]the name of the attachment.gif\0
  [type]image/gif\0
  [content]<bytes comprising the content until end-of-file>


EXTRACT
-------
MIME parts (including attachments) may also be extracted as files to the user
home directory. The extracted file name is always prefixed by "SOYMAIL-" to
hopefully avoid clashes with any other files in this area.  The rest of the
name is derived from the part name.  For implementation simplicity the
generated file name is constrained to ODS-2 compliance.

Textual files have a stream-LF format.  Any others have a fixed 512 byte
format.  Extracted message parts where file names have no file type have an
extension guessed by examining the MIME content-type; text/plain become .TXT,
text/html become .HTML, and message/rfc822 become .LIS (message listings).


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
---------------
02-SEP-2010  MGD  bugfix; AttachComposeList() uninitialised pointer
                    if attachment file did not produce a content-type
13-JUL-2008  MGD  AttachComposeList() accomodate [attachment-MIME-types]
22-MAR-2007  MGD  AttachComposeList() JavaScript-enable the [attach] button
                  AttachUpload() massage MSIE filenames with full path
10-FEB-2007  MGD  Purveyor support removed
21-OCT-2006  MGD  AttachComposeList() checkbox to 'delete all attachments'
                    after a successful message send 
27-JUN-2006  MGD  AttachFileExtract() to extract message parts as
                    user-accessable files into the user home directory
25-MAR-2006  MGD  AttachView() small accomodation for Purveyor
15-MAR-2006  MGD  AttachFile() ensure any file system path is stripped
                    from the attachment name before saving (MSIE)
05-FEB-2006  MGD  support saving message as "message/rfc822" 'attachment'
                  bugfix; AttachComposeList() attachment size calculation
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

#pragma nomember_alignment

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

/* VMS related header files */
#include <descrip.h>
#include <rmsdef.h>
#include <ssdef.h>
#include <stsdef.h>
#include <uaidef.h>

/* application header file */
#include "soymail.h"
#include "config.h"
#include "attach.h"
#include "message.h"

#define FI_LI "ATTACH", __LINE__

/* global storage */

/* external storage */

extern BOOL  Debug,
             WatchEnabled;

extern char  SoftwareVn[];

extern CONFIG_DATA  SoyMailConfig;
extern VMS_MAIL_USER  VmsMailUser;

/* prototypes */
int sys$fao (__unknown_params);
int sys$getuai (__unknown_params);
int sys$numtim (__unknown_params);

/*****************************************************************************/
/*
Assemble the request data related to attachment management.  Return true to
indicate that there was attachment management undertaken and that the calling
function should do no more, false to indicate none and the calling function
should display a page.
*/

BOOL AttachComposeRequest (REQUEST_DATA *rdptr)

{
   int  cnt,
        AttachFileCount;
   char  *cptr, *sptr;
   char  AttachmentName [256],
         ButtonName [64];

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

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

   CGIVARNULL (cptr, "FORM_ATTACH_BTN");
   if (!cptr) CGIVARNULL (cptr, "FORM_ATTACH_DELETE_BTN");
   if (cptr)
   {
      /**********************/
      /* attachment buttons */
      /**********************/

      if (LangSame ("attach_file", cptr))
         AttachUpload (rdptr);
      else
      if (LangSame ("attach_delete", cptr))
         AttachDelete (rdptr, NULL);
      else
         ErrorExit (SS$_BUGCHECK, FI_LI);

      return (FALSE);
   }
   else
   {
      /**************************/
      /* individual attachments */
      /**************************/

      CGIVARNULL (cptr, "FORM_ATTACH_COUNT");
      if (cptr)
         AttachFileCount = atoi(cptr);
      else
         AttachFileCount = 0;

      for (cnt = 1; cnt <= AttachFileCount+5; cnt++)
      {
         sprintf (ButtonName, "FORM_ATTACH_DELETE_%d", cnt);
         CGIVARNULL (cptr, ButtonName);
         if (cptr)
         {
            sprintf (AttachmentName, "FORM_ATTACH_ID_%d", cnt);
            CGIVARNULL (sptr, AttachmentName);
            if (!sptr) ErrorExit (SS$_BUGCHECK, FI_LI);
            AttachDelete (rdptr, sptr);

            return (FALSE);
         }

         sprintf (ButtonName, "FORM_ATTACH_VIEW_%d", cnt);
         CGIVARNULL (cptr, ButtonName);
         if (cptr)
         {
            sprintf (AttachmentName, "FORM_ATTACH_ID_%d", cnt);
            CGIVARNULL (sptr, AttachmentName);
            if (!sptr) ErrorExit (SS$_BUGCHECK, FI_LI);

            /* NOTE: if successful this returns from here! */
            if (AttachView (rdptr, sptr)) return (TRUE);

            return (FALSE);
         }
      }
   }

   return (FALSE);
}

/*****************************************************************************/
/*
Generate a list of attachments available for sending from the message 
composition page. 
*/

void AttachComposeList (REQUEST_DATA *rdptr)

{
   int  cnt, status,
        AttachFileCount,
        AttachFileTotal,
        ButtonWidth,
        FileSizeBytes;
   char  *cptr, *fnptr, *sptr, *zptr,
         *CheckedPtr,
         *ContentTypePtr,
         *NamePtr;
   char  AttId [64],
         BlockBuffer [512],
         CbxName [256];
   stat_t  FstatBuffer;
   FILE  *fp;
   VMS_MAIL_USER  *muptr;

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

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

   muptr = &VmsMailUser;

   if (!muptr->VmsMailFullDirectoryLength) ErrorExit (SS$_BUGCHECK, FI_LI);

   cptr = LangFor("compose_button_width");
   ButtonWidth = atoi(cptr);
   if (ButtonWidth <= 0 || ButtonWidth > 10) ButtonWidth = 5;

   fprintf (stdout,
"<!-- message attachments begin -->\n\
<p class=\"css_p06\">\
\
<table width=\"100%%\" cellpadding=\"1\" cellspacing=\"5\" \
class=\"css_panel_1\" align=\"center\">\n");

   /* first up count the number of attachment files */
   AttachFileTotal = 0;
   cptr = FileSpecSearch (1, muptr->VmsMailFullDirectory, "SOYMAIL_*.ATT");
   while (cptr)
   {
      AttachFileTotal++;
      cptr = FileSpecSearch (1, NULL, NULL);
   }

   /* list each one of those attachment files */
   AttachFileCount = 0;
   fnptr = FileSpecSearch (FALSE, muptr->VmsMailFullDirectory, "SOYMAIL_*.ATT");
   while (fnptr)
   {
      AttachFileCount++;

      /* get the digits from the file name */
      for (cptr = fnptr; *cptr; cptr++);
      while (cptr > fnptr && *cptr != ']') cptr--;
      if (*cptr == ']') cptr++;
      while (*cptr && !isdigit(*cptr)) cptr++;
      zptr = (sptr = AttId) + sizeof(AttId);
      while (isdigit(*cptr) && sptr < zptr) *sptr++ = *cptr++;
      if (sptr > zptr) ErrorExit (SS$_BUGCHECK, FI_LI);
      *sptr = '\0';

      NamePtr = ContentTypePtr = "";

      fp = fopen (fnptr, "r", "ctx=bin");
      if (!fp)
      {
         status = vaxc$errno;
         sprintf (BlockBuffer, "%s: %s", fnptr, SysGetMsg(status));
         cptr = BlockBuffer;
      }
      else
      {
         if (fstat (fileno(fp), &FstatBuffer) != 0) FstatBuffer.st_size = -1;
         FileSizeBytes = FstatBuffer.st_size;
         memset (BlockBuffer, 0, sizeof(BlockBuffer));
         fread (BlockBuffer, sizeof(BlockBuffer), 1, fp);
         fclose (fp);
         if (!strncmp (BlockBuffer, "[soyMAIL-version]", 17))
         {
            cptr = BlockBuffer + 17;
            while (*cptr) cptr++;
            cptr++;
            if (!strncmp (cptr, "[name]", 6))
            {
               cptr += 6;
               NamePtr = cptr;
               /* adjust for the number of bytes in the internal data */
               while (*cptr) cptr++;
               cptr++;
               if (!strncmp (cptr, "[type]", 6))
               {
                  cptr += 6;
                  ContentTypePtr = cptr;
                  while (*cptr) cptr++;
                  cptr++;
                  if (!strncmp (cptr, "[content]", 9))
                  {
                     cptr += 9;
                     FileSizeBytes -= cptr - BlockBuffer;
                  }
                  else
                     NamePtr = "***ERROR4***";
               }
               else
                  NamePtr = "***ERROR3***";
            }
            else
               NamePtr = "***ERROR2***";
         }
         else
            NamePtr = "***ERROR1***";
      }

      /* a 'delete all' button on the first line of the attachment listing */
      if (AttachFileCount == 1)
      {
         CGIVARNULL (CheckedPtr, "FORM_ATTACH_CHK_DELETE");
         if (CheckedPtr)
         {
            CGIVARNULL (CheckedPtr, "FORM_ATTACH_CBX_DELETE");
            if (CheckedPtr) CheckedPtr = " checked";
         }
         else
            CheckedPtr = " checked";
         if (!CheckedPtr) CheckedPtr = "";

         fprintf (stdout,
"<tr><td width=\"10%%\" nowrap>\
<input class=\"css_button_panel_1\" style=\"width:%dem;\" type=\"submit\" \
name=\"attach_delete_btn\" value=\"%s\" \
onclick=\"window.removeEventListener(\'beforeunload\',doBeforeUnload);\
return true;\">&nbsp;&nbsp;\
<input type=\"hidden\" name=\"attach_chk_delete\" value=\"1\">\
<input class=\"css_button_panel_1\" type=\"checkbox\" \
style=\"border:none;background-color:transparent;\" \
name=\"attach_cbx_delete\" value=\"1\"%s>\
</td><td colspan=\"2\" style=\"padding-left:15px;\">",
                  ButtonWidth,
                  LangFor("attach_delete"),
                  CheckedPtr);
      }
      else
         fprintf (stdout,
"<tr><td width=\"10%%\"></td><td colspan=\"2\" style=\"padding-left:15px;\">");

      /* check if the attachment checkbox is currently checked */
      sprintf (CbxName, "FORM_PART_CBX_%s", AttId);
      CGIVARNULL (CheckedPtr, CbxName);
      if (CheckedPtr) CheckedPtr = " checked";
      if (!CheckedPtr && !strcmp (rdptr->AttachmentName, NamePtr))
         CheckedPtr = " checked";
      if (!CheckedPtr) CheckedPtr = "";

      if (cptr = SoyMailConfig.AttachmentMimeTypes)
      {
         /********************/
         /* separate window? */
         /********************/

         while (*cptr)
         {
            sptr = ContentTypePtr;
            if (WatchEnabled) WatchThis (FI_LI, "MIME \"!AZ\" !AZ", sptr, cptr);
            while (*cptr && !isspace(*cptr) && *sptr &&
                   tolower(*cptr) == tolower(*sptr))
            {
               cptr++;
               sptr++;
            }
            if (!*cptr || isspace(*cptr)) { cptr = NULL; break; }
            while (*cptr && !isspace(*cptr)) cptr++;
            while (*cptr && isspace(*cptr)) cptr++;
         }
      }
      /* cptr is non-NULL to indicate hit */
      if (!cptr)
      {
         /******/
         /* no */
         /******/

         /* default or internally handled type */
         fprintf (stdout,
"<input type=\"hidden\" name=\"attach_id_%d\" value=\"%s\">\
<input class=\"css_button_panel_1\" type=\"submit\" \
name=\"attach_delete_%d\" value=\"%s\" \
onclick=\"window.removeEventListener(\'beforeunload\',doBeforeUnload);\
return true;\">&nbsp;\
<input class=\"css_button_panel_1\" type=\"submit\" \
name=\"attach_view_%d\" value=\"%s\">",
                  AttachFileCount, AttId,
                  AttachFileCount, LangFor("delete"),
                  AttachFileCount, LangFor("view"));
      }
      else
      {
         /*******/
         /* yes */
         /*******/

         /* not an internally handled type (with JavaScript) */
         fprintf (stdout,
"<input type=\"hidden\" name=\"attach_id_%d\" value=\"%s\">\
<input class=\"css_button_panel_1\" type=\"submit\" \
name=\"attach_delete_%d\" value=\"%s\" \
onclick=\"window.removeEventListener(\'beforeunload\',doBeforeUnload);\
return true;\">&nbsp;\
<script language=\"JavaScript\">\
composeAttachment%d = \"%s%s%s?view:%s\";\n\
document.write(\'<input class=\"css_button_panel_1\" \
type=\"submit\" name=\"attach_view_%d\" value=\"%s\" \
onclick=\"targetBlank();window.open(composeAttachment%d);return false;\">\');\
</script>",
                  AttachFileCount, AttId,
                  AttachFileCount, LangFor("delete"),
                  AttachFileCount, rdptr->FormAction,
                  rdptr->FormAction[strlen(rdptr->FormAction)-1] == '/' ?
                     "" : "/",
                  HTML_ESCAPE(NamePtr), AttId,
                  AttachFileCount, LangFor("view"),
                  AttachFileCount);
      }

      fprintf (stdout,
"<script language=\"JavaScript\">\
composeAttachment%d = \"%s%s%s?view:%s\";\n\
document.write(\'<input class=\"css_button_panel_1\" \
style=\"border-left-width:0px;\" \
type=\"submit\" name=\"attach_view_%d\" value=\"^\" \
onclick=\"targetBlank();window.open(composeAttachment%d);return false;\">\');\
</script>\
&nbsp;&nbsp;\
<input class=\"css_button_panel_1\" type=\"checkbox\" \
style=\"border:none;background-color:transparent\" \
name=\"part_cbx_%s\" value=\"1\"%s>\
&nbsp;\
%s\
&nbsp;&nbsp;<I>(%d %s)</I>\
</td></tr>\n",
               AttachFileCount, rdptr->FormAction,
               rdptr->FormAction[strlen(rdptr->FormAction)-1] == '/' ?
                  "" : "/",
               HTML_ESCAPE(NamePtr), AttId,
               AttachFileCount,
               AttachFileCount,
               AttId, CheckedPtr, HTML_ESCAPE(NamePtr),
               FileSizeBytes > 1024 ? FileSizeBytes / 1024: FileSizeBytes,
               FileSizeBytes > 1024 ? "kB": "bytes");

      fnptr = FileSpecSearch (FALSE, NULL, NULL);
   }

   /* attachment file upload button */
   fprintf (stdout,
"<tr><td width=\"10%%\" style=\"vertical-align:top;\">\
<input class=\"css_button_panel_1\" style=\"width:%dem;\" type=\"submit\" \
name=\"attach_btn\" id=\"attach_btn\" value=\"%s\" \
onclick=\"window.removeEventListener(\'beforeunload\',doBeforeUnload);\
return startWorking();\">\
</td><td style=\"padding-left:15px;\">\
<div class=\"css_button_panel_1 css_input_type_file\">\
<input class=\"css_text_1\" id=\"attach_file\" \
type=\"file\" name=\"attach_file\" size=\"50\" \
onchange=\"abo=document.getElementById(\'attach_btn\');\
abo.click();abo.disabled=true;\">\
<span onclick=\"document.getElementById(\'attach_file\').click();\
return false;\">%s</span>\
</div>\
</td><td width=\"100%%\"></td></tr>\n\
</table>\n\
<input type=\"hidden\" name=\"attach_count\" value=\"%d\">\n\
<!-- message attachments end -->\n\n",
               ButtonWidth,
               LangFor("attach_file"),
               LangFor("select_file"),
               AttachFileCount);
}                              

/*****************************************************************************/
/*
Upload an attachment content (from the message composition page).
*/

void AttachUpload (REQUEST_DATA *rdptr)

{
   int  DataLength;
   char  *cptr, *sptr, *zptr,
         *DataPtr,
         *NamePtr,
         *TypePtr;
   VMS_MAIL_USER  *muptr;

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

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

   muptr = &VmsMailUser;

   if (!muptr->VmsMailFullDirectoryLength) ErrorExit (SS$_BUGCHECK, FI_LI);

   DataPtr = CgiLibVar ("FORM_ATTACH_FILE");
   if (WatchEnabled)
      WatchThis (FI_LI, "CGIVAR !AZ=!AZ", "FORM_ATTACH_FILE", DataPtr); 
   if (CgiLibBodyIsMultipartFormData())
      DataLength = CgiLibHtmlDeEntify (DataPtr);
   else
      DataLength = strlen(DataPtr);

   CGIVAR (NamePtr, "FORM_ATTACH_FILE_MIME_FILENAME");
   if (!(NamePtr && *NamePtr))
   {
      StatusMessage (FI_LI, 1, "%s", LangFor("attach_none_supplied"));
      return;
   }
   /* MSIE (what else?) provides the file name with a full path */
   for (cptr = NamePtr; *cptr; cptr++);
   while (cptr > NamePtr && *cptr != '/' && *cptr != '\\' && *cptr != ']')
      cptr--;
   if (*cptr == '/' || *cptr == '\\' || *cptr == ']') NamePtr = cptr + 1;

   CGIVAR (TypePtr, "FORM_ATTACH_FILE_MIME_CONTENT_TYPE");
   if (!(TypePtr && *TypePtr)) TypePtr = "application/octet-stream";

   /* if the attachment file was not successfully writen */
   if (!AttachFileSave (NamePtr, TypePtr, DataPtr, DataLength)) return;

   /* save the file name for checkbox checking in the attachment list */
   zptr = (sptr = rdptr->AttachmentName) + sizeof(rdptr->AttachmentName)-1;
   for (cptr = NamePtr; *cptr && sptr < zptr; *sptr++ = *cptr++);
   *sptr = '\0';

   StatusMessage (FI_LI, 0, "%s &quot;%s&quot;.",
                  LangFor("attach_upload_done"), NamePtr);
}                              

/*****************************************************************************/
/*
Save one or more attachment content (from the message read page).
*/

void AttachSave
(
REQUEST_DATA *rdptr,
BOOL ExtractFile
)
{
   BOOL  ok,
         AttachMessage = FALSE;
   int  cnt,
        DataLength,
        FileCount,
        PartCount;
   char  *cptr,
         *DataPtr,
         *NamePtr,
         *TypePtr;
   char  AttachCbxName [32],
         PartTypeField [32];
   $DESCRIPTOR (ReportDsc, "");
   MIME_DATA  *mdptr;
   USER_OPTIONS  *uoptr;
   VMS_MAIL_MSG  *vmptr;
   VMS_MAIL_USER  *muptr;

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

   if (Debug) fprintf (stdout, "AttachSave() %d\n", ExtractFile);

   muptr = &VmsMailUser;

   if (!muptr->VmsMailFullDirectoryLength) ErrorExit (SS$_BUGCHECK, FI_LI);

   uoptr = &rdptr->UserOptions;
   muptr = &VmsMailUser;
   vmptr = &muptr->VmsMailMsg;

   vmptr->MessageId = rdptr->MessageId;
   CallMailMessageGet (muptr, vmptr);
   if (VMSnok (muptr->MailVmsStatus))
   {
      StatusMessage (FI_LI, 1, "MAIL: %s.", SysGetMsg(muptr->MailVmsStatus));
      return;
   }

   CGIVARNULL (cptr, "FORM_PART_CBX_0");
   if (cptr) AttachMessage = TRUE;

   MessageBodyProcess (vmptr);

   FileCount = 0;

   if (AttachMessage)
   {
      /* the full message */
      cptr = vmptr->Subject;
      if (!*cptr) cptr = LangFor("attach_message");
      if (ExtractFile)
         cptr = AttachFileExtract (cptr, "message/rfc822",
                                   vmptr->BodyPtr, vmptr->BodyLength);
      else
         cptr = AttachFileSave (cptr, "message/rfc822",
                                vmptr->BodyPtr, vmptr->BodyLength);
      if (!cptr) return;
      AttachReportFile (&ReportDsc, cptr);
      FileCount++;
   }

   CGIVAR (cptr, "FORM_PART_COUNT");
   if (cptr)
   {
      PartCount = atoi(cptr);
      if (PartCount > 512) ErrorExit (SS$_BUGCHECK, FI_LI);
   }
   else
      PartCount = 0;

   for (mdptr = vmptr->MimeDataPtr; mdptr; mdptr = mdptr->NextMimePtr)
   {
      if (mdptr->ContentTypeIsMultipart) continue;

      for (cnt = 1; cnt <= PartCount; cnt++)
      {
         sprintf (AttachCbxName, "FORM_PART_CBX_%d", cnt);
         CGIVAR (cptr, AttachCbxName);
         if (!cptr) continue;
         if (mdptr->NamePtr)
            NamePtr = mdptr->NamePtr;
         else
            NamePtr = mdptr->PartName;
         if (!strsame (cptr, NamePtr, -1)) continue;

         /* use the content-type from the form */
         sprintf (PartTypeField, "FORM_PART_TYPE_%d", cnt);
         CGIVARNULL (TypePtr, PartTypeField);
         if (!TypePtr || !*TypePtr) TypePtr = mdptr->ContentTypePtr;

         if (MimeDecProcessPart (mdptr))
         {
            DataPtr = mdptr->PartContentPtr;
            DataLength = mdptr->PartContentLength;

            /* if writing the attachment file was not successful */
            if (ExtractFile)
               cptr = AttachFileExtract (NamePtr, TypePtr, DataPtr, DataLength);
            else
               cptr = AttachFileSave (NamePtr, TypePtr, DataPtr, DataLength);
            if (!cptr) return;

            AttachReportFile (&ReportDsc, cptr);
            FileCount++;
         }
         break;
      }
   }

   if (FileCount)
   {
      if (ExtractFile)
         StatusMessage (FI_LI, 0, "%s %s.", LangFor("attach_extract_done"),
                        HTML_ESCAPE(ReportDsc.dsc$a_pointer));
      else
         StatusMessage (FI_LI, 0, "%s %s.", LangFor("attach_save_done"),
                        HTML_ESCAPE(ReportDsc.dsc$a_pointer));
   }
   else
      StatusMessage (FI_LI, 1, "%s", LangFor("attach_none_supplied"));
}                              

/*****************************************************************************/
/*
Creat a null-terminated string described by the supplied descriptor containing
a comma-separated list of part or file names.  The string is dynamically built
over successive calls to this function.
*/

int AttachReportFile
(
struct dsc$descriptor_s *DscPtr,
char *NamePtr
)
{
   int  len;
   char  sbuf [1024];

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

   if (Debug) fprintf (stdout, "AttachReportFile() |%s|\n", NamePtr);

   len = PrintFao (sbuf, sizeof(sbuf), "!AZ\"!AZ\"",
                   DscPtr->dsc$w_length ? ", " : "", NamePtr);
   if (DscPtr->dsc$w_length)
      DscPtr->dsc$a_pointer = CgiLibVeeMemRealloc (DscPtr->dsc$a_pointer,
                                       DscPtr->dsc$w_length + len + 8);
   else
      DscPtr->dsc$a_pointer =  CgiLibVeeMemCalloc (DscPtr->dsc$w_length + len + 8);
   strcpy (DscPtr->dsc$a_pointer + DscPtr->dsc$w_length, sbuf);
   DscPtr->dsc$w_length += len;
   return (DscPtr->dsc$w_length);
}                              

/*****************************************************************************/
/*
Write an attachment to the file system.
Return pointer if the write was ok, NULL otherwise.
*/

char* AttachFileSave
(
char *NamePtr,
char *TypePtr,
char *DataPtr,
int DataLength
)
{
   static $DESCRIPTOR (AttIdFaoDsc, "!4ZL!2ZL!2ZL!2ZL!2ZL!2ZL!2ZL!2ZL\0");
   static $DESCRIPTOR (FileNameFaoDsc, "SOYMAIL_!AZ.ATT\0");
   static int  FileCount;
   static unsigned short  NumTime [7];

   int  retval, status;
   char  *cptr, *fnptr;
   char  AttId [20],
         FileName [64];
   FILE  *fp;
   VMS_MAIL_USER  *muptr;
   $DESCRIPTOR (AttIdDsc, AttId);
   $DESCRIPTOR (FileNameDsc, FileName);

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

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

   muptr = &VmsMailUser;

   if (!muptr->VmsMailFullDirectoryLength) ErrorExit (SS$_BUGCHECK, FI_LI);

   /* ensure any file system path is stripped from the file name (MSIE) */
   NamePtr = AttachStripPath (NamePtr);

   /* only generate the one time-stamp per request */
   if (!NumTime[0]) sys$numtim (&NumTime, 0);

   /* two digits */
   if (FileCount++ > 99) ErrorExit (SS$_BUGCHECK, FI_LI);

   /* create an attachment ID based on the time and counter */
   sys$fao (&AttIdFaoDsc, NULL, &AttIdDsc,
            NumTime[0], NumTime[1], NumTime[2], NumTime[3],
            NumTime[4], NumTime[5], NumTime[6], FileCount);

   sys$fao (&FileNameFaoDsc, NULL, &FileNameDsc, AttId);

   fnptr = CallMailFileIn (muptr, NULL, FileName);
   if (WatchEnabled) WatchThis (FI_LI, "ATTACHMENT !AZ", fnptr); 

   fp = fopen (fnptr, "w", "ctx=bin", "rfm=udf", "rat=none");
   if (!fp)
   {
      status = vaxc$errno;
      StatusMessage (FI_LI, 1, "%s: %s.", NamePtr, SysGetMsg(status));
      return (NULL);
   }

   retval = write (fileno(fp), "[soyMAIL-version]", 17);
   /* include the terminating null */
   if (retval > 0) retval = write (fileno(fp), SoftwareVn,
                                               strlen(SoftwareVn)+1);

   if (retval > 0) retval = write (fileno(fp), "[name]", 6);
   /* include the terminating null */
   if (retval > 0) retval = write (fileno(fp), NamePtr, strlen(NamePtr)+1);

   if (retval > 0) retval = write (fileno(fp), "[type]", 6);
   /* include the terminating null */
   if (retval > 0) retval = write (fileno(fp), TypePtr, strlen(TypePtr)+1);

   if (retval > 0) retval = write (fileno(fp), "[content]", 9);
   if (retval > 0) retval = write (fileno(fp), DataPtr, DataLength);
   fclose (fp);

   if (retval <= 0)
   {
      status = vaxc$errno;
      StatusMessage (FI_LI, 1, "%s: %s.", NamePtr, SysGetMsg(status));
      /* remove remnants of failed write */
      while (!remove (fnptr));
      return (NULL);
   }

   /* purge the versions back to a maximum of 1 */
   strcat (fnptr, ";-1");
   while (!remove (fnptr));

   return (NamePtr);
}                              

/*****************************************************************************/
/*
Extract and write an attachment as a file to the user home directory.
Return pointer if the write was ok, NULL otherwise.

The extracted file name is always prefixed by "SOYMAIL-" to hopefully avoid
clashes with any other files in this area.  The rest of the name is derived
from the part name.  For simplicity the generated file name is constrained to
ODS-2 compliance.  Textual files have a stream-LF format.  Any others have an
fixed 512 byte format.
*/

char* AttachFileExtract
(
char *NamePtr,
char *TypePtr,
char *DataPtr,
int DataLength
)
{
   static unsigned long  UaiContext = -1;
   static char  FileName [256+8],
                UaiDefDev [1+31+1],
                UaiDefDir [1+63+1];
   static VMS_ITEM_LIST3  UaiItems [] = 
   {
      { sizeof(UaiDefDev)-1, UAI$_DEFDEV, &UaiDefDev, 0 },
      { sizeof(UaiDefDir)-1, UAI$_DEFDIR, &UaiDefDir, 0 },
      { 0,0,0,0 }
   };

   BOOL  TypePeriod;
   int  retval, status;
   char  *cptr, *fnptr, *sptr, *zptr;
   FILE  *fp;
   VMS_MAIL_USER  *muptr;
   $DESCRIPTOR (FileNameDsc, FileName);
   $DESCRIPTOR (UserNameDsc, "");

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

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

   muptr = &VmsMailUser;

   if (!muptr->VmsMailFullDirectoryLength) ErrorExit (SS$_BUGCHECK, FI_LI);

   UserNameDsc.dsc$a_pointer = muptr->UserName;
   UserNameDsc.dsc$w_length = muptr->UserNameLength;

   status = sys$getuai (0, &UaiContext, &UserNameDsc, &UaiItems, 0, 0, 0);
   if (Debug) fprintf (stdout, "sys$getuai() %%X%08.08X\n", status);
   if (VMSnok (status))
   {
      StatusMessage (FI_LI, 1, "GETUAI: %s.", SysGetMsg(status));
      return (NULL);
   }

   UaiDefDev[UaiDefDev[0]+1] = '\0';
   UaiDefDir[UaiDefDir[0]+1] = '\0';

   /* ensure any file system path is stripped from the file name (MSIE) */
   NamePtr = AttachStripPath (NamePtr);

   /* create a file name for in the user's home directory */
   TypePeriod = FALSE;
   zptr = (sptr = FileName) + sizeof(FileName)-8;
   for (cptr = UaiDefDev+1; *cptr && sptr < zptr; *sptr++ = *cptr++);
   for (cptr = UaiDefDir+1; *cptr && sptr < zptr; *sptr++ = *cptr++);
   /* keep the name ODS-2 compliant for now */
   fnptr = sptr;
   /* hyphen so it cannot collide with soyMAIL's own files! */
   for (cptr = "SOYMAIL-"; *cptr && sptr < zptr; *sptr++ = *cptr++);
   cptr = NamePtr;
   while (*cptr && *cptr != '.' && sptr < zptr && sptr - fnptr < 39)
   {
      if (isalnum(*cptr)) *sptr++ = toupper(*cptr); else *sptr++ = '_';
      cptr++;
   }
   if (*cptr == '.')
   {
      /* hit a (hopefully) type delimitting period */
      TypePeriod = TRUE;
      if (sptr < zptr && sptr - fnptr < 39) *sptr++ = *cptr++;
      while (*cptr && sptr < zptr && sptr - fnptr < 39)
      {
         if (isalnum(*cptr)) *sptr++ = toupper(*cptr); else *sptr++ = '_';
         cptr++;
      }
   }
   if (*cptr)
   {
      /* not end of name yet, probe for the file type */
      while (*cptr && *cptr != '.') cptr++;
      if (*cptr)
      {
         TypePeriod = TRUE;
         fnptr = sptr;
         if (sptr < zptr) *sptr++ = *cptr++;
         while (*cptr && sptr < zptr && sptr - fnptr < 39)
         {
            if (isalnum(*cptr)) *sptr++ = toupper(*cptr); else *sptr++ = '_';
            cptr++;
         }
      }
   }
   *sptr = '\0';
   if (!TypePeriod)
   {
      /* no file-type, have a guess using the content-type */
      if (strsame (TypePtr, "text/plain", -1))
         cptr = ".TXT";
      else
      if (strsame (TypePtr, "text/html", -1))
         cptr = ".HTML";
      else
      if (strsame (TypePtr, "message/rfc822", -1))
         cptr = ".LIS";
      else
         cptr = "";
      while (*cptr && sptr < zptr) *sptr++ = *cptr++;
      *sptr = '\0';
   }
   fnptr = FileName;

   if (WatchEnabled) WatchThis (FI_LI, "EXTRACT !AZ", fnptr); 

   if (strsame (TypePtr, "text/", 5) ||
       strsame (TypePtr, "message/", 8))
      fp = fopen (fnptr, "w", "ctx=bin", "rfm=stmlf");
   else
      fp = fopen (fnptr, "w", "ctx=bin", "rfm=fix", "mrs=512", "rat=none");
   if (!fp)
   {
      status = vaxc$errno;
      StatusMessage (FI_LI, 1, "%s: %s.", NamePtr, SysGetMsg(status));
      return (NULL);
   }

   retval = write (fileno(fp), DataPtr, DataLength);
   fclose (fp);

   if (retval <= 0)
   {
      status = vaxc$errno;
      StatusMessage (FI_LI, 1, "%s: %s.", NamePtr, SysGetMsg(status));
      /* remove remnants of failed write */
      strcat (fnptr, ";0");
      remove (fnptr);
      return (NULL);
   }

   /* return at the point the file name begins */
   cptr = FileName + UaiDefDev[0] + UaiDefDir[0];
   return (cptr);
}                              

/*****************************************************************************/
/*
Ensure any file system path is stripped from the file name (MSIE).
*/

char* AttachStripPath (char *NamePtr)

{
   char  *cptr;

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

   if (Debug) fprintf (stdout, "AttachStripPath() %s\n", NamePtr);

   if (*NamePtr != '/' &&
       *NamePtr != '\\' &&
       memcmp(NamePtr, "./", 2) &&
       memcmp(NamePtr, "../", 3) &&
       !(isalpha(NamePtr[0]) && NamePtr[1] == ':'))
      return (NamePtr);

   for (cptr = NamePtr; *cptr && *cptr != '/'; cptr++);
   if (*cptr)
   {
      while (*cptr) cptr++;
      while (cptr > NamePtr && *cptr != '/') cptr--;
      if (*cptr == '/') cptr++;
      NamePtr = cptr;
   }
   else
   {
      for (cptr = NamePtr; *cptr && *cptr != '\\'; cptr++);
      if (*cptr)
      {
         while (*cptr) cptr++;
         while (cptr > NamePtr && *cptr != '\\') cptr--;
         if (*cptr == '\\') cptr++;
         NamePtr = cptr;
      }
   }

   return (NamePtr);
}                              

/*****************************************************************************/
/*
Delete attachment files, a single one or all.
If 'FileName' is NULL delete all attachment files.
*/

void AttachDelete
(
REQUEST_DATA *rdptr,
char *AttId
)
{
   int  status;
   char  *cptr, *fnptr, *sptr, *zptr;
   char  FileName [64];
   VMS_MAIL_USER  *muptr;

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

   if (Debug) fprintf (stdout, "AttachDelete() %s\n", AttId);

   muptr = &VmsMailUser;

   if (!muptr->VmsMailFullDirectoryLength) ErrorExit (SS$_BUGCHECK, FI_LI);

   if (AttId)
   {
      zptr = (sptr = FileName) + sizeof(FileName)-1;
      for (cptr = "SOYMAIL_"; *cptr && sptr < zptr; *sptr++ = *cptr++);
      for (cptr = AttId; *cptr && sptr < zptr; *sptr++ = *cptr++);
      for (cptr = ".ATT"; *cptr && sptr < zptr; *sptr++ = *cptr++);
      *sptr = '\0';

      fnptr = CallMailFileIn (muptr, NULL, FileName);

      if (WatchEnabled) WatchThis (FI_LI, "ATTACHMENT DELETE !AZ", fnptr); 

      if (remove (fnptr))
      {
         status = vaxc$errno;
         StatusMessage (FI_LI, 1, "%s: %s.", AttId, SysGetMsg(status));
         return;
      }
      /* remove all versions */
      while (!remove (fnptr));

      StatusMessage (FI_LI, 0, "%s.", LangFor("attach_delete_done"));
   }
   else
   {
      cptr = FileSpecSearch (1, muptr->VmsMailFullDirectory, "SOYMAIL_*.ATT");
      while (cptr)
      {
         if (WatchEnabled) WatchThis (FI_LI, "ATTACHMENT DELETE !AZ", fnptr); 

         fnptr = CallMailFileIn (muptr, NULL, cptr);

         if (remove (fnptr))
         {
            status = vaxc$errno;
            StatusMessage (FI_LI, 1, "%s: %s.", fnptr, SysGetMsg(status));
            return;
         }
         /* remove all versions */
         while (!remove (fnptr));

         cptr = FileSpecSearch (1, NULL, NULL);
      }

      StatusMessage (FI_LI, 0, "%s.", LangFor("attach_delete_all"));
   }
}                              

/*****************************************************************************/
/*
The message composition page contains buttons for viewing attachment content.
*/

BOOL AttachView
(
REQUEST_DATA *rdptr,
char *AttId
)
{
   static $DESCRIPTOR (FileNameFaoDsc, "SOYMAIL_!AZ.ATT\0");

   int  status,
        ContentLength,
        DataLength,
        FileSizeBytes;
   char  *cptr, *fnptr, *czptr,
         *ContentTypePtr,
         *DataPtr;
   char  DataBuffer [4096],
         FileName [64];
   stat_t  FstatBuffer;
   FILE  *fp;
   VMS_MAIL_USER  *muptr;
   $DESCRIPTOR (FileNameDsc, FileName);

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

   if (Debug) fprintf (stdout, "AttachView() %s\n", FileName);

   muptr = &VmsMailUser;

   if (!muptr->VmsMailFullDirectoryLength) ErrorExit (SS$_BUGCHECK, FI_LI);

   sys$fao (&FileNameFaoDsc, NULL, &FileNameDsc, AttId);
   fnptr = CallMailFileIn (muptr, NULL, FileName);
   if (WatchEnabled) WatchThis (FI_LI, "ATTACHMENT !AZ", fnptr); 

   fp = fopen (fnptr, "r", "ctx=bin");
   if (!fp)
   {
      status = vaxc$errno;
      StatusMessage (FI_LI, 1, "%s: %s.", AttId, SysGetMsg(status));
      if (strcmp (rdptr->CgiRequestMethodPtr, "GET")) return (FALSE);
      CgiLibResponseError (FI_LI, status, AttId);
      return (TRUE);
   }
   else
   {
      if (fstat (fileno(fp), &FstatBuffer)) FstatBuffer.st_size = -1;
      FileSizeBytes = FstatBuffer.st_size;
      DataLength = read (fileno(fp), DataBuffer, sizeof(DataBuffer));
   }

   czptr = (cptr = DataBuffer) + DataLength;
   if (strncmp (cptr, "[soyMAIL-version]", 17))
   {
      StatusMessage (FI_LI, 1, "%s: %s.", AttId, SysGetMsg(SS$_BUGCHECK));
      if (strcmp (rdptr->CgiRequestMethodPtr, "GET")) return (FALSE);
      CgiLibResponseError (FI_LI, SS$_BUGCHECK, AttId);
      return (TRUE);
   }
   cptr += 17;
   while (*cptr && cptr < czptr) cptr++;
   if (cptr < czptr) cptr++;
   if (strncmp (cptr, "[name]", 6))
   {
      StatusMessage (FI_LI, 1, "%s: %s.", AttId, SysGetMsg(SS$_BUGCHECK));
      if (strcmp (rdptr->CgiRequestMethodPtr, "GET")) return (FALSE);
      CgiLibResponseError (FI_LI, SS$_BUGCHECK, AttId);
      return (TRUE);
   }
   cptr += 6;
   while (*cptr && cptr < czptr) cptr++;
   if (cptr < czptr) cptr++;
   if (strncmp (cptr, "[type]", 6))
   {
      StatusMessage (FI_LI, 1, "%s: %s.", AttId, SysGetMsg(SS$_BUGCHECK));
      if (strcmp (rdptr->CgiRequestMethodPtr, "GET")) return (FALSE);
      CgiLibResponseError (FI_LI, SS$_BUGCHECK, AttId);
      return (TRUE);
   }
   cptr += 6;
   /* now should be pointing at the content type */
   ContentTypePtr = cptr;
   /* skip over the type */
   while (*cptr && cptr < czptr) cptr++;
   if (cptr < czptr) cptr++;
   if (strncmp (cptr, "[content]", 9))
   {
      StatusMessage (FI_LI, 1, "%s: %s.", AttId, SysGetMsg(SS$_BUGCHECK));
      if (strcmp (rdptr->CgiRequestMethodPtr, "GET")) return (FALSE);
      CgiLibResponseError (FI_LI, SS$_BUGCHECK, AttId);
      return (TRUE);
   }
   /* now should be pointing at the content itself (finally) */
   cptr += 9;
   ContentLength = FileSizeBytes - (cptr - DataBuffer);
   /* what remains in the data buffer */
   DataLength -= cptr - DataBuffer;

   CgiLibResponseHeader (200, ContentTypePtr,
                         "Content-Length: %d\n%s",
                         ContentLength, rdptr->LoginSetCookiePtr);

   while (DataLength)
   {
      fwrite (cptr, DataLength, 1, stdout);
      DataLength = read (fileno(fp), cptr = DataBuffer, sizeof(DataBuffer));
   }

   fclose (fp);

   return (TRUE);
}                              

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

