/*****************************************************************************/
/*
                                  newmail.c

New mail checking and notification.

Contains a neat little experiment in Ajax

  http://ajaxpatterns.org/

which has turned out quite satisfactorily (see NewMailScriptRefresh()).


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
---------------
09-JUN-2013  MGD  NewMailRefreshXMLHttpRequest() and NewMailXMLHttpRequest()
                    provide a more modern new mail check at the server
                    [check-newmail-ajax] reverts to the older IFRAME check
                  audio alert WAVs obsoleted in favour of MP3s
27-MAR-2011  MGD  NewMailScriptAjax() refresh if message count decreases
14-FEB-2010  MGD  NewMailAt()
28-OCT-2008  MGD  NewMailLoginFail()
31-APR-2008  MGD  NewMailScriptRefresh() add 'beeps' for
                    browsers/environments not supporting WAVs
10-FEB-2007  MGD  Purveyor support removed
30-DEC-2006  MGD  NewMailScriptRefresh() and NewMailScriptAjax() cater
                    for MSIE initial IFRAME load to avoid disruptive
                    initial 'check new mail' message in status panel
06-OCT-2006  MGD  propagate login credential cookie
20-JUN-2006  MGD  CallMailUserDiskQuota() and MessageDiskQuotaUsed() notifies
                    the client with each folder open once disk quota reaches
                    the percentage specified by default or configuration
25-MAR-2006  MGD  NewMailPersonalYouGotMail() small accomodation for Purveyor
23-MAR-2006  MGD  'checking for new mail' uses setWorking() progress dots
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 <ints.h>
#include <file.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stat.h>
#include <unixio.h>

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

/* application header file */
#include <soymail.h>
#include <address.h>
#include <config.h>
#include <gmt.h>
#include <request.h>
#include <message.h>
#include <mta.h>
#include <newmail.h>
#include <sendmail.h>

#define FI_LI "NEWMAIL", __LINE__

/* prototypes */

int sys$gettim (__unknown_params);
int lib$sub_times (__unknown_params);

/* external storage */

extern BOOL  Debug,
             WatchEnabled;

extern char  LocalHostName[];

extern CONFIG_DATA  SoyMailConfig;
extern VMS_MAIL_USER  VmsMailUser;

/*****************************************************************************/
/*
Called by MainMenuRequest() after a [new] button or by the .submit() of
NewMailScriptAjax() with NEWMAIL_REFRESH non-empty.
*/

void NewMailMessage (REQUEST_DATA *rdptr)

{
   int  cnt, idx, status;
   int  *midptr;
   char  *cptr;
   USER_OPTIONS  *uoptr;
   REQUEST_DATA  *sdptr;
   VMS_MAIL_MSG  *vmptr;
   VMS_MAIL_USER  *muptr;

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

   if (WatchEnabled) WatchThis (FI_LI, "NewMailMessage()");

   muptr = &VmsMailUser;
   vmptr = &muptr->VmsMailMsg;
   uoptr = &rdptr->UserOptions;
   sdptr = (REQUEST_DATA*)rdptr->StateDataPtr;

   CallMailGoToNewMail (muptr);

   strcpy (rdptr->MailFileName, "MAIL");
   rdptr->MailFileNameLength = 4;
   strcpy (rdptr->FolderName, "NEWMAIL");
   rdptr->FolderNameLength = 7;

   if (VMSok (muptr->MailVmsStatus))
   {
      if (sdptr->NewMailValid)
      {
         /* newmail count in the state data is valid */
         if (rdptr->NewMessages > sdptr->NewMessages)
         {
            rdptr->NewMailNoticed = TRUE;
            if (uoptr->YouGotMail[0]) rdptr->NewMailPlay = TRUE;
            if (uoptr->YouGotMailAt[0] &&
                uoptr->YouGotMailAt[0] != '#' &&
                uoptr->YouGotMailAt[0] != '!')
               rdptr->NewMailAt = TRUE;
         }
         else
         if (sdptr->NewMailNoticed)
         {
            CGIVARNULL (cptr, "FORM_NEWMAIL_REFRESH");
            if (cptr && *cptr == 'N')
               rdptr->NewMailNoticed = TRUE;
            else
               rdptr->NewMailNoticed = FALSE;
            rdptr->NewMailPlay = rdptr->NewMailAt = FALSE;
         }
      }

      if (rdptr->NewMailNoticed)
      {
         StatusMessage (FI_LI, -1, "&nbsp;%s", LangFor("n_e_w_m_a_i_l"));
         if (rdptr->NewMailAt) NewMailAt (rdptr);
      }
      else
      {
         char  FolderOpened [512];
         status = CallMailUserDiskQuota (muptr);
         if (VMSnok (status))
            StatusMessage (FI_LI, 1, "DISK-QUOTA: %s.", SysGetMsg(status));
         else
         {
            sprintf (FolderOpened, LangFor("folder_opened"),
                     HTML_ESCAPE(rdptr->FolderName),
                     vmptr->MessageSelectedCount);
            StatusMessage (FI_LI, 0, "%s%s",
                           FolderOpened, MessageDiskQuotaUsed(rdptr));
         }
      }
   }
   else
      StatusMessage (FI_LI, 1, "MAIL: %s.", SysGetMsg(muptr->MailVmsStatus));

   MessageFolderPage (rdptr);
}


/*****************************************************************************/
/*
Does what NewMailScriptRefresh() did with handcrafted Ajax, checks for new mail
message on the server, using the (now) better established XMLHttpRequest()
JavaScript object.
*/

char* NewMailRefreshXMLHttpRequest (REQUEST_DATA *rdptr)

{
   USER_OPTIONS  *uoptr;

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

   if (WatchEnabled) WatchThis (FI_LI, "NewMailRefreshXMLHttpRequest()");

   if (!NewMailInclude (rdptr)) return (NULL);

   uoptr = &rdptr->UserOptions;

   /* note that the newmail count is valid (used by NewMailmessage()) */
   rdptr->NewMailValid = TRUE;

   fprintf (stdout,
"<script language=\"JavaScript\">\n\
var lang_CheckingNewMail = \"%s\";\n\
var lang_CheckNewMailIn = \"%s\";\n\
var lang_Minutes = \"%s\";\n\
var lang_n_e_w_m_a_i_l = \"%s\";\n\
var soy_CgiServerName = \'%s\';\n\
var soy_CheckNewMailMins = %d;\n\
var soy_CurrentNewMessages = %d;\n",
            LangFor("checking_newmail"),
            LangFor("check_newmail_in"),
            LangFor("minutes"),
            LangFor("n_e_w_m_a_i_l"),
            CgiLibVar("SERVER_NAME"),
            uoptr->CheckNewMailMins,
            rdptr->NewMessages);

   if (!uoptr->YouGotMail[0])
      fprintf (stdout, "var soy_YouGotMailPath = null;\n");
   else
   if (!strcmp (uoptr->YouGotMail, "(beep!)"))
      fprintf (stdout, "var soy_YouGotMailPath = '(beep!)';\n");
   else
   if (!strcmp (uoptr->YouGotMail, "(personal)"))
   {
      if (rdptr->UserAgent == SOY_USER_AGENT_MSIE)
         fprintf (stdout,
"var soy_YouGotMailPath = \'%s/yougotmail.mp3?yougotmail.mp3';\n",
                  rdptr->FormAction);
      else
         fprintf (stdout, "soy_YouGotMailPath = \'%s?yougotmail.mp3';\n",
                  rdptr->FormAction);
   }
   else
      fprintf (stdout, "var soy_YouGotMailPath = \'%s%s.mp3';\n",
               SOY_THEME_PATH, uoptr->YouGotMail);

   fputs ("</script>\n", stdout);

   if (uoptr->CheckNewMailAjax) return (NewMailScriptRefresh(rdptr));

   return ("refreshNewMail(soy_CheckNewMailMins);");
}

/*****************************************************************************/
/*
Just like pulling teeth!

I started off with the (currently) recommended method of using <OBJECT>s for
the audio but had too many issues getting it to work satisfactorily across
Mozilla, MSIE and Opera.  I have gone to the deprecated - but obviously more
widely supported and uniform - <embed>.  Also needs to be supported for the
[listen] button in the user options page.

Place the required HTML and JavaScript infrastructure in place on the message
listing page for the NEWMAIL folder.  This infrastruture implements the new
mail checking and audio as well as textual notification.

There are an number of variants used for the refresh here:

  o  full refresh - where the full folder message listing page is refreshed
                    to check for any new mail (fairly expensive).

                    The is a checkbox on the options page alongside the
                    check newmail minutes for switching to this behaviour
                    (as a fallback for untested browser behaviours).

  o  Ajax 1 - for everything but MSIE; where an IFRAME is used as a
              background engine to 'poll' soyMAIL for an increase in the
              new mail count.  When there is no new mail it just returns
              a small HTML reposne with JavaScript code to refresh the new
              mail status information.  When new mail is detected (actually
              any change in the new message count) it returns an HTML response
              containing JavaScript code to submit the form of the parent
              soyMAIL window causing the page with the listing to refresh.
              This is quite lightweight compared to the full refresh method.

  o  Ajax 2 - a minor variant for MSIE (what else? :-) which complains about
              'secure and non-secure' items on a secure (SSL accessed) page.
              Ajax 1 suppresses an initial (and unnecessary) poll when a
              NEWMAIL message listing page is requested by not putting any
              src= in that frame's HTML.  Then at the first scheduled poll
              uses JavaScript and the DOM to dynamiclly change the IFRAME
              document location and reload.  MSIE complains about that.
 
              There is a second checkbox on the options page alongside
              the check newmail minutes for switching to this behaviour
              (as a fallback for untested browser behaviours).

This has been tested against Win32 versions of Firefox 1.5, Mozilla 1.7, Opera
8.5 and MSIE 6.0.
*/

char* NewMailScriptRefresh (REQUEST_DATA *rdptr)

{
   char  *cptr, *sptr, *zptr;
   USER_OPTIONS  *uoptr;

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

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

   if (!NewMailInclude (rdptr)) return (NULL);

   uoptr = &rdptr->UserOptions;

   if (rdptr->UserAgent == SOY_USER_AGENT_MSIE)
      fprintf (stdout,
"<iframe name=\"newmail_iframe\" id=\"newmail_iframe\" \
style=\"display:none;\" src=\"%s?999999\">\
</iframe>\n",
               rdptr->FormAction);
   else
      fprintf (stdout,
"<iframe name=\"newmail_iframe\" id=\"newmail_iframe\" \
style=\"display:none;\">\
</iframe>\n");

   return ("refreshNewMail(soy_CheckNewMailMins);");
}

/*****************************************************************************/
/*
Respond to the XMLHttpRequest() from the client enquiring about new mail.
The request query string is tested early in request handling and if it contains
a number this function is called.  The number in the query string is the
current (at the browser) new mail count.  A comparison is made to the user's
new mail count.
*/

void NewMailXMLHttpRequest (REQUEST_DATA *rdptr)

{
   int  NewMailCount;
   USER_OPTIONS  *uoptr;

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

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

   uoptr = &rdptr->UserOptions;

   if (uoptr->CheckNewMailAjax)
   {
      NewMailScriptAjax(rdptr);
      return;
   }

   NewMailCount = atoi(rdptr->CgiQueryStringPtr);

   if (rdptr->NewMessages > NewMailCount)
      /* number of messages has increased */
      CgiLibResponseHeader (202, "application/x-soyMAIL",
                            "Content-Length: 0\n%s",
                            rdptr->LoginSetCookiePtr);
   else
   if (rdptr->NewMessages < NewMailCount)
      /* number of messages has decreased (e.g. deleted by another agent) */
      CgiLibResponseHeader (205, "application/x-soyMAIL",
                            "Content-Length: 0\n%s",
                            rdptr->LoginSetCookiePtr);
   else
      /* number of messages has remained the same */
      CgiLibResponseHeader (204, "application/x-soyMAIL",
                            "Content-Length: 0\n%s",
                            rdptr->LoginSetCookiePtr);
}

/*****************************************************************************/
/*
This is the workhorse function for the Ajax-style new mail checker.  The
request query string is tested early in request handling and if it contains a
number this function is called.  The number in the query string is the current
(at the browser) new mail count.  A comparison is made to the user's new mail
count.  If different the small HTML page returned contains JavaScript to
refresh the IFRAME's parent (reload the entire page using the <form> submit()
method), the main soyMAIL message listing window.  If the new mail count has
not changed (usually increased) then just a similarly small HTML page
containing JavaScript to refresh the new mail status information.  Neat huh?!!
*/

void NewMailScriptAjax (REQUEST_DATA *rdptr)

{
   int  NewMailCount;

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

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

   CgiLibResponseHeader (200, "text/html",
                         "Script-Control: X-content-encoding-gzip=0\n%s",
                         rdptr->LoginSetCookiePtr);

   /* it's the AJAX IFRAME requesting a newmail count */
   NewMailCount = atoi(rdptr->CgiQueryStringPtr);
   if (NewMailCount == 999999)
   {
      /* magic number for ... */
      fprintf (stdout, "<!-- MSIE initial IFRAME load -->\n");
   }
   else
   if (rdptr->NewMessages < NewMailCount)
   {
      /* number of messages has decreased (e.g. deleted by another agent) */
      fprintf (stdout,
"<script language=\"JavaScript\">\
parent.document.getElementById(\'newmail_refresh\').value=\'DECREMENT\';\
parent.document.getElementById(\'soymail_form\').submit();\
</script>\n");
   }
   else
   if (rdptr->NewMessages > NewMailCount)
   {
      /* number of messages has increased */
      fprintf (stdout,
"<script language=\"JavaScript\">\
parent.document.getElementById(\'newmail_refresh\').value=\'NEWMAIL\';\
parent.document.getElementById(\'soymail_form\').submit();\
</script>\n");
   }
   else
   {
      fprintf (stdout,
"<script language=\"JavaScript\">\
parent.window.clearWorking();\
parent.window.refreshNewMailStatus(null);\
</script>\n");
   }
}

/*****************************************************************************/
/*
An Ajax-style new mail checker request has failed to login.  Return a response
that will cause the parent window to refresh (and thereby result in the login
page to be presented).
*/

void NewMailLoginFail (REQUEST_DATA *rdptr)

{
   USER_OPTIONS  *uoptr;

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

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

   uoptr = &rdptr->UserOptions;

   if (uoptr->CheckNewMailAjax)
   {
      /* old-fashioned Ajax :-) */
      CgiLibResponseHeader (200, "text/html",
                            "Script-Control: X-content-encoding-gzip=0\n%s",
                            rdptr->LoginSetCookiePtr);

      fprintf (stdout,
"<script language=\"JavaScript\">\
parent.document.getElementById(\'soymail_form\').submit();\
</script>\n");
   }
   else
   {
      /* XMLHttpRequest() */
      CgiLibResponseHeader (403, "application/x-soyMAIL",
                            "Content-Length: 0\n%s",
                            rdptr->LoginSetCookiePtr);
   }
}

/*****************************************************************************/
/*
Returns the personalised mail notification audio content stored in the user'
mail directory.   For network efficiency, reduced latency, and overall better
performance keep track of the modification datum of the file being served by
this function.
*/

void NewMailPersonalYouGotMail (REQUEST_DATA *rdptr)

{
   int  fd, retval, status,
        DataLength;
   char  *cptr, *fnptr,
         *DataPtr;
   char  LastModifiedGmt [32];
   stat_t  StatBuffer;
   USER_OPTIONS  *uoptr;
   REQUEST_DATA  *sdptr;
   VMS_MAIL_USER  *muptr;

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

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

   muptr = &VmsMailUser;
   uoptr = &rdptr->UserOptions;
   sdptr = (REQUEST_DATA*)rdptr->StateDataPtr;

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

   if (stat (fnptr, &StatBuffer)) ErrorExit (vaxc$errno, FI_LI);

   GmtBinTimeToString (LastModifiedGmt, NULL, StatBuffer.st_mtime);

   CGIVARNULL (cptr, "HTTP_IF_MODIFIED_SINCE");
   if (cptr)
   {
      if (strsame (cptr, LastModifiedGmt, -1))
      {
         /* not modified */
         CgiLibResponseHeader (304, NULL, "Content-Length: 0\n");
         return;
      }
   }

   fd = open (fnptr, O_RDONLY, 0, "ctx=stm", "shr=get");
   if (!fd) ErrorExit (vaxc$errno, FI_LI);

   DataPtr =  CgiLibVeeMemCalloc (StatBuffer.st_size);
   if (!DataPtr) ErrorExit (vaxc$errno, FI_LI);

   DataLength = read (fd, DataPtr, StatBuffer.st_size);
   if (DataLength <= 0) ErrorExit (vaxc$errno, FI_LI);
   close (fd);

   /* http://www.garykessler.net/library/file_sigs.html */
   /* MP3 magic hex 49 44 33 or hex ff fb */
   if (!(DataPtr[0] == 0xff &&
         (DataPtr[1] & 0xf6) > 0xf0 &&
         (DataPtr[2] & 0xf0) != 0xf0))
      ErrorExit (SS$_BUGCHECK, FI_LI);

   CgiLibResponseHeader (200, "audio/mpeg",
"Content-Length: %d\n\
Expires: %s\n\
Last-Modified: %s\n\
%s\
Script-Control: X-content-encoding-gzip=0\n",
      StatBuffer.st_size,
      LastModifiedGmt,
      LastModifiedGmt,
      rdptr->LoginSetCookiePtr);

   fwrite (DataPtr, DataLength, 1, stdout);

   CgiLibVeeMemFree (DataPtr);
}

/*****************************************************************************/
/*
Notify of new mail by sending an advisary mail message (hopefully elsewhere).
*/

void NewMailAt (REQUEST_DATA *rdptr)

{
   int  NewMessageCount;
   char  *cptr, *sptr, *zptr,
         *SelfPtr,
         *SmtpServerPtr;
   char  MsgText [256],
         UpCaseRemoteUser [256];
   REQUEST_DATA  *sdptr;
   USER_OPTIONS  *uoptr;

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

   if (WatchEnabled) WatchThis (FI_LI, "NewMailAt()");

   uoptr = &rdptr->UserOptions;
   sdptr = (REQUEST_DATA*)rdptr->StateDataPtr;
   NewMessageCount = rdptr->NewMessages - sdptr->NewMessages;

   cptr = LangFor("option_soy_new_soymail_messages");

   if (AddressIsRfc(uoptr->YouGotMailAt))
   {
      SmtpServerPtr = SoyMailConfig.SmtpServerHost;
      if (!SmtpServerPtr) SmtpServerPtr = "localhost";

      SelfPtr = ComposeSelfAddress (rdptr);

      sprintf (MsgText, "%d %s %s",
               NewMessageCount, cptr, SelfPtr);

      cptr = MtaMsgSmtpToServer (SmtpServerPtr,
                                 SelfPtr,
                                 uoptr->YouGotMailAt,
                                 MsgText, NULL,
                                 MsgText, NULL);
      if (cptr) StatusMessage (FI_LI, 1, "SMTP: %s", HTML_ESCAPE(cptr));
   }
   else
   {
      /* autogenous authentication login may mean this is in lower-case */
      zptr = (sptr = UpCaseRemoteUser) + sizeof(UpCaseRemoteUser)-1;
      for (cptr = rdptr->RemoteUser;
           *cptr && sptr < zptr;
           *sptr++ = toupper(*cptr++));
      *sptr = '\0';

      sprintf (MsgText, "%d %s %s@%s",
               NewMessageCount, cptr, UpCaseRemoteUser, LocalHostName);

      cptr = SendMailMessage (UpCaseRemoteUser, uoptr->YouGotMailAt,
                              NULL, MsgText, MsgText);
      if (cptr) StatusMessage (FI_LI, 1, "MAIL: %s", HTML_ESCAPE(cptr));
   }
}

/*****************************************************************************/
/*
All good reasons not to include the NEWMAIL refresh functionality.
*/

BOOL NewMailInclude (REQUEST_DATA *rdptr)

{
   /*********/
   /* begin */
   /*********/

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

   if (rdptr->ThisPageIdent != PAGE_FOLDER) return (FALSE);
   if (!rdptr->UserOptions.CheckNewMailMins) return (FALSE);
   if (rdptr->SearchData.InProgress) return (FALSE);
   if (strcmp (rdptr->MailFileName, "MAIL") ||
       strcmp (rdptr->FolderName, "NEWMAIL")) return (FALSE);
   return (TRUE);
}

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

