/*****************************************************************************/
#ifdef COMMENTS_WITH_COMMENTS
/*
                                   Login.c


    THE GNU GENERAL PUBLIC LICENSE APPLIES DOUBLY TO ANYTHING TO DO WITH
                    AUTHENTICATION AND AUTHORIZATION!

    This package is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; version 2 of the License, or any later
    version.

>   This package is distributed in the hope that it will be useful,
>   but WITHOUT ANY WARRANTY; without even the implied warranty of
>   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
>   GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.


DESCRIPTION
-----------
This module provides the core of a soyMAIL autogenous (inherent, internal)
authentication system.  It does so by accepting username and password form
field values from a login page and authenticating these credentials using
internal code.  Once authenticated it propagates the username/password strings
from request to request, along with some other credential maintenance data, so
that the client can be transparently re-authenticated periodically to keep the
session valid.

Obviously propagating sensitive data such as passwords around over a network
has it's dangers.  With normal HTTP authorization fields this happens all the
time (the credentials are base-64 encoded but not encrypted making them trivial
to discover).  This is why it is best for such transactions to occur using SSL. 
Certainly with SYSUAF based authentication you would imagine this to be
mandatory.  So it shouldn't be a real issue as far as the transaction goes but
such information also shouldn't be trivially recovered from either the HTML
or other browser source, or from cookie content.  Hence soyMAIL encrypts the
credential data using the RC4 algorithm and a minimum 192 bit key (24
characters) supplied from site configuration.  This makes it non-trivial to
recover such data.  Also, when soyMAIL is logged-out the content of those
credentials are removed from the browser.

The best-case would be to authenticate every request.  This would obviously
generate some significant authentication load on a busy system.  Therefore to
reduce this potential load soyMAIL only authenticates periodically, at initial
login (obviously) and then every five minutes by default, although this is
configurable.  Requests during the interim have the credentials unencrypted and
provided they pass sanity checks by soyMAIL the username is used for that
request.

Sessions are also tracked for idle time.  If more than this period occurs
between successive requests soyMAIL will present the login page and require
reauthentication.  (See IDLE SESSION TIMEOUT below.)  Idle time is one hour by
default but is configurable.  This might be considered relatively inconvenient
when about to [send] a message composed some time ago but by using cookies,
once re-authenticated, automatically (see below), or manually, return to the
previous page and continue with the refreshed cookie content.  To effectively
disable idle timeouts set it to something large (e.g. 30000000 seconds - ~1
year :-) Careful though, it's a U**x integer time and will overflow easily!
 

RC4 ENCRYPTION
--------------
I am certainly not a cryptography specialist so be sure to take anything (and
everything) I say here with a large pinch of salt!

RC4 is a very commonly deployed, efficient and arguably safe-enough encryption
scheme.

  http://en.wikipedia.org/wiki/Rc4

While not necessarily authoritative the above reference states:

  "In cryptography, RC4 (also known as ARC4 or ARCFOUR) is the most
   widely-used software stream cipher and is used in popular protocols
   such as Secure Sockets Layer (SSL) (to protect Internet traffic) ...
   While remarkable in its simplicity, RC4 falls short of the high
   standards of security set by cryptographers ... However, some systems
   based on RC4 are secure enough for practical use."

And into that latter category soyMAIL (hopefully) falls.  Keep your browser
secure while in use and [logout] to clear the actual encrypted content when
appropriate.

To assist in keeping the credential encryption secure ensure the SOYMAIL_CONFIG
file is protected for general access and ensure the [login-secret] string used
for the encryption key contains a whole range of characters.  soyMAIL insists
this key is at least 24 characters in length.  An *example* key might be

  [login-secret]  aic84+-[QsfOPa,dk;'12ndjve64_90mhd43m*2$

The secret can be modified at any time but consider that client sessions
currently in-progress will require to re-login the next time they access
soyMAIL.

For implementation detail see module RC4.C

The encrypted data is additionally base-64 encoded to make it suitable for
character-oriented schemes such as HTTP cookies.


CREDENTIALS COOKIE
------------------
Some mechanism for conveying the login credentials from request to request was
required.  This is often done via a form field or request query string
component, either directly or as a 'session ID' of some description.  This
involves the explicit inclusion of such a field with every form or request
query string generated by the application.  It effectively can be hidden in a
form but must appear obvious in GET request query strings (and as a consequence
is included in access logs).  It was decided due to the above disadvantages to
use HTTP cookies.  For all their issues and the disdain in which they are held
by some they are an effective and transparent mechanism for providing state
information of all sorts with HTTP requests and so have become the chosen
mechanism for soyMAIL autogenous authentication.  Don't like them?  Well use
server-configured HTTP authorization!


INTERNAL COOKIE STRUCTURE
-------------------------
The cookie value is a base-64 encoded, RC4-encrypted series of null-terminated
ASCII strings, along with a couple of fixed, single-byte fields.  Some strings
represent numbers, others text.  When a cookie value is generated the data
strings are preceded by a variable-length, ASCII hexadecimal number (generated
from a binary time component).  This serves to add some quasi-indeterminate
data to the mix and results in startlingly different encrypted/encoded cookie
values each time one is generated (and is therefore perhaps of benefit to
encryption security through an obfuscation factor).

Following the hex number is a single, non-zero byte, indicating the cookie
structure version, and used to detect possible future changes to the structure,
then a single character, either 'L' or 'C' to differentiate the following two
structures:

1) The 'login' cookie; used during the login process.  After the hexadecimal
data is an 'L' and then a number representing the count of the login attempts. 
The second string contains either the username of any previous session, or is
empty.  This is used to determine whether the new login is for the same user as
any previous session, and so if the login count should be used to return to the
last relevant page in the browser history.  One of these might look something
like, plain and encrypted/encoded (where \0 are the null-terminators, and \1
the current version byte):

  D2B45\0\1L1\0DANIEL\0
  OqycxO8ffs1lPBqIZEHLlg==

2) The 'credentials' cookie; used once logged-in to maintain the session and
provide the credentials for periodic revalidation.  Following the hexadecimal
number is a 'C'.  Then a decimal number, the U**x time in seconds, after which
the session is considered idle.  Following that another decimal number, the
U**x time in seconds, after which the session credentials are revalidated
against the authentication source.  Following these are four strings; the IP
address of the client (from CGI variable REMOTE_ADDR), any alias user name
supplied during login (or '*' to indicate none), the authenticated (usually
VMS) username, and the supplied password.

A credentials cookie looks something like, plain and encrypted/encoded:

  51F0A88\0\1C1160471530\01160467960\0192.168.1.2\0*\0DANIEL\0PASSWORD\0
  SZX9Lf8uefaXl3fsAzazonP0mV6pTa6hWg296nb/EnE8rIxdH4Lxj1KBWrCmfOVZtrdQnnBUisZmP

It should be emphasised that the credential data is transmitted and stored
encrypted.  It is only plain data when being handled internally by soyMAIL.


LOGIN PAGE
----------
The core content of the login page is supplied via a file, by default
[.THEME]LOGIN.HTML, though the configuration directive [login-page] can specify
an alternative.  If a local customisation is undertaken it is suggested to copy
the default to say something like [.THEME]_LOGIN.HTML and modify and configure
that.  This approach offers considerable flexibility in providing
language-specific and site-specific information at the point of soyMAIL login.
DO NOT CHANGE anything like form field names, etc.  Only the markup!


ACME AUTHENTICATOR
------------------
The Authentication and Credentials Management Extensions (ACME) subsystem
provides authentication and persona-based credential services.  Applications
use these services to enforce authentication policies defined by ACME agents
running in the context of the ACME_SERVER process.  Note that there are some
behavioural differences between the ACME and SYSUAF authenticators.

Only for VMS V7.3 or later.

Relies on the image being INSTALLed with /AUTHPRIVILEGE=(IMPERSONATE) so that
it can use $ACMW.


SYSUAF AUTHENTICATOR
--------------------
On platforms that do not support ACME a composite authenticator is provided
using $SCAN_INTRUSION and $GETUAI.  This implementation checks for disusered
flag, etc., primary password, password expiry (primary and secondary).  It also
checks and maintains the intrusion database.  Every effort has been made to
ensure this authenticator is robust but no guarantees.  Note that there are
some behavioural differences between the ACME and SYSUAF authenticators.  The
SYSUAF authenticator is a much simpler (and incomplete) implementation in
comparison to ACME.  I'd much rather rely on an Engineering maintained service
such as $ACM!  But it's there is you want it.

Relies on the image being INSTALLed with /AUTHPRIVILEGE=(SECURITY) so that it
can use $SCAN_INTRUSION.


PASSWORD CHANGE
---------------
soyMAIL provides configurable password change.  Both the ACME and SYSUAF
authenticators can perform this.  The change can be triggered by password
expiry or the presence of a '[x] change password' checkbox on the login page. 
Obviously ACME is going to provide a more robust function.  The SYSUAF based
password change attempts to allow for all the wrinkles basic password
maintenance provides (character and password length restrictions, etc.) but
provides no dictionary checking, password history, etc.

ENABLING PASSWORD MODIFIFICATION ON PLATFORMS THAT DON'T SUPPORT ACME SHOULD
NOT BE DONE LIGHTLY.  The ortha pruvides no garruntees!!

The configuration directive should be set to the default change page

  [login-change-page]  SOYMAIL_THEME:CHANGE.HTML

or, as with the login page, to a local customisation of the default

  [login-change-page]  SOYMAIL_THEME:_CHANGE.HTML


AGENT AUTHENTICATOR
-------------------
If the [login-agent] configuration directive exists then it activates an
external authentication DCL procedure within a subprocess.  The DCL procedure
must be specified following an '@' symbol and must have execute permission for
the scripting account.  For example:

  [login-agent] @cgi-bin:[000000]soymail_authage.com

Optional parameters can be provided as part of the configuration directive:

  [login-agent] @cgi-bin:[000000]soymail_authage.com "param1" "PARAM2"

When activated the procedure has a further three parameters appended to any
supplied in the above configuration directive; "<username>", "<password>" and
"<remote_addr>", to be used in the authentication processing.

The procedure should exit with any success status (e.g. SS$_NORMAL) to indicate
successful authentication or any error status (e.g. SS$_NOPRIV) to indicate
authentication failure.  To indicate the credentials are OK but they do not
allow access return SS$_CONTROLO (a success status).

This is a (very simple) example.

  $ SET ON
  $ OK = 1
  $ NOPRIV = 36
  $ P1 = F$EDIT(P1,"UPCASE")
  $ IF P1 .EQS. "CURLY" .AND. P2 .EQS. "JeromE" THEN EXIT OK
  $ IF P1 .EQS. "LARRY" .AND. P2 .EQS. "lOUIs" THEN EXIT OK
  $ IF P1 .EQS. "MOE" .AND. P2 .EQS. "harrY" THEN EXIT OK
  $ EXIT NOPRIV

This would probably be more like a working one.

  $ SET ON
  $ SOYAUTH = "$location:SOYAUTH.EXE"
  $ SOYAUTH "''P1'" "''P2'" "''P3'"
  $ EXIT $STATUS

This is a(nother very simple) example showing an autogenous authorization
agent denying access to the path /noaccess/ using a SS$_CONTROLO exit status.

  $ SET ON
  $ OK = 1
  $ NOPRIV = 36
  $ CONTROLO = 1545
  $ P1 = F$EDIT(P1,"UPCASE")
  $ IF P1 .EQS. "CURLY" .AND. P2 .EQS. "JeromE" THEN GOTO ISOK
  $ IF P1 .EQS. "LARRY" .AND. P2 .EQS. "lOUIs" THEN GOTO ISOK
  $ IF P1 .EQS. "MOE" .AND. P2 .EQS. "harrY" THEN GOTO ISOK
  $ EXIT NOPRIV
  $ ISOK:
  $ IF WWW_PATH_INFO .EQS. "/noaccess/" THEN EXIT CONTROLO
  $ EXIT OK

See further comments under AUTHORIZED PUBLIC ACCESS in CONFIG.C.


LOGIN ALIAS
-----------
For autogenous authentication the configuration directive [login-alias] allows
the user-supplied 'username' strings to be mapped to other strings before being
authenticated.  An example use might be using the user email address, or local
mailbox name, as the 'username' and mapping that to a VMS username before
authenticating against the SYSUAF.  The alias mapping is performed using a
case-insensitive match.  The format is one alias mapping per line.

  [login-alias]
  Mark.Daniel/DANIEL
  Fred.Bloggs/BLOGGS

If a match is not made and no mapping has occurred the default (drop-through)
behaviour is to use the original user-supplied 'username' string and so in the
above configuration both "Mark.Daniel" and "DANIEL" would authenticate against
the DANIEL account, "Fred.Bloggs" and "BLOGGS" against BLOGGS.  Of course any
other supplied username could also authenticate directly using the username. 
To prevent any other than an aliased username being used for authentication
conclude the list with "*/-".

  [login-alias]
  Mark.Daniel/DANIEL
  Fred.Bloggs/BLOGGS
  */-

In the above example only "Mark.Daniel" and "Freg.Bloggs" can be supplied to
allow authentication.  The (somewhat redundant) mapping "*/*" will explicitly
direct authentication to occur using the supplied 'username' (the default
drop-through behaviour anyway).


IDLE SESSION TIMEOUT
--------------------
What is required is to save the *entire* state of a request being processed by
soyMAIL.  That way, if a session requires re-authentication, the session can be
continued in a non-disruptive and transparent fashion.

This has proved particularly problematic (thanks to Bill Korendyk testing and
discovering limitations with the initial approach).  Originally soyMAIL would
use JavaScript and the browser history object to return to the page immediately
preceding the (re-)login page.  Unfortunately some browsers (Firefox at least)
somehow did not restore the content of edited <textarea>s.  This was initially
noticed with an idle compose page.  Edits *looked* as if they were OK but what
was subsequently sent from the browser to soyMAIL actually contained the
unedited, original content of the <textarea>.  OK, one instance of this was
enough to put the kibosh on the entire scheme!!

The subsequent approach is less elegant, if innovative and lateral in it's
approach, and relies on a degree of kludginess.  If a browser will not perform
the state maintenance it must be done elsewhere.  With soyMAIL's POST-based
mechanism the obvious place where all the state is contained is in the request
body.  The internal components of any given request are complex and impractical
to maintain individually, let alone the vast range of possible soyMAIL
requests.  The request body must be managed as a whole.

What soyMAIL now does is have the login page put the entire request body (the
request immediately preceding the re-authentication login page) into a form
field as cerealised (serialised) data (propagating that from login page to
login page as necessary).  Upon successful re-authentication the
post-authentication processing looks for and detects this buffered request
body.  There's a chicken-and-egg issue here.  To get the buffered request body
it must read and extract the form variables from the POSTed login page body. 
It then needs to transparently substitute the buffered body for the current
login page body.  It does this by obfuscating the FORM_ variables from the
current body using an (unobtrusive) CGILIB kludge introduced especially for
this purpose (see CgiLib__VarList()).  It then has CGILIB read the form
variables from the buffered body (see soyMAIL ProcessRequest()).  Seems to work
reasonably well.

It has been tested with re-authenticate-interrupted 2.5MB attachment uploads. 
It's a bit slower and the browser is a bit latent as it processes the huge form
field(s).  It does work though!  So, try not to get distracted for too long
between selecting the file and hitting the [attach] button. :-)


VERSION HISTORY
---------------
04-SEP-2023  MGD  LoginSetCookie() Set-Cookie: SameSite=Strict
05-MAY-2009  MGD  LoginAcmeVerifyUser() only employ ACME$_TARGET_DOI_NAME
                    when configured otherwise ACME$LATEST_ENABLED_AGENT_LIST
22-APR-2009  MGD  LoginAgentVerifyUser() SS$_CONTROLO (1545) indicates
                    that the credentials are OK but the user no access
                    to the area (introduced to support public authorization)
11-JUN-2008  MGD  LoginAlias() to map user-supplied login name to user name
                  login cookie structure version changed to '2'
                    with the addition of the login alias field
22-JUN-2008  MGD  allow change of (expired) password (via login dialogs)
                  configurable login type (via [login-type], default NETWORK)
09-DEC-2007  MGD  bugfix; LoginPage() remove HTML-escaping of login message
15-OCT-2007  MGD  LoginAgentVerifyUser() and [login-agent] external
                    agent authenticator (see description above)
03-OCT-2007  MGD  LoginUaiUser() behave more like LoginAcmeVerifyUser()
                    authenticator by only checking password expiry flags
30-SEP-2007  MGD  if the request URI is different to the script name (due to
                    creative mapping for non-script access URLs) make the
                    cookie path "/" instead of the script name
15-JUL-2007  MGD  ACME return of LGI$_RESTRICT (login source restriction)
                  is ignored if authentication OK ([login-acme-no-restrict])
13-JAN-2007  MGD  refined idle timeout re-authentication handling
07-OCT-2006  MGD  soyMAIL autogenous authentication and logout
*/
#endif /* COMMENTS_WITH_COMMENTS */
/*****************************************************************************/

#ifndef LOGIN_ACME
   /* only available for V7.3 and later */
#  if (__CRTL_VER >= 70300000)
#     define LOGIN_ACME 1
#  else
#     define LOGIN_ACME 0
#  endif
#endif

#ifdef WASD_VMS_V6
#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 <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>

/* VMS related header files */
#include <descrip.h>
#include <jpidef.h>
#include <libdef.h>
#include <libdtdef.h>
#include <lib$routines.h>
#include <prvdef.h>
#include <ssdef.h>
#include <starlet.h>
#include <stsdef.h>
#include <uaidef.h>

#ifndef UAI$M_PWDMIX
#define UAI$M_PWDMIX 0x2000000
#endif

/* this image is built with ACME capabilities */
#if LOGIN_ACME 
#include <utcblkdef.h>
#include <acmedef.h>
#include <acmemsgdef.h>
#ifndef LGI$_RESTRICT
#  define LGI$_RESTRICT 13861020
#endif
#endif

#ifndef ACME$_PWDEXPIRED
/* this will be used to signal password expiry from SYSUAF functions too! */
#  define ACME$_PWDEXPIRED 122332570
#endif

#define ILE3 ile3  /* because of the __VMS_VER */

/* application related header files */
#include "soymail.h"
#include "cereal.h"
#include "config.h"
#include "login.h"
#include "rc4.h"

#define FI_LI "LOGIN", __LINE__

/******************/
/* global storage */
/******************/

/* soyMAIL 'l'ogin 'c'redentials */
static char  LoginPrivateCookieName [] = "soyMAILlc";

/* soyMAIL 'p'ublic login 'c'redentials */
static char  LoginPublicCookieName [] = "soyMAILpc";

/* two-byte 'counted string', to allow detection of cookie structure changes */
static char  LoginCookieVersion [] = { 1, 2 };

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

extern BOOL  Debug,
             WatchEnabled;

extern char DocType[];

extern CONFIG_DATA  SoyMailConfig;

/*****************************************************************************/
/*
This function sets the login credentials cookie value 'login' data and provides
the autogenous login page.  The displayed HTML content of this page is
dynamically loaded from a configuration location (allowing site and/or language
customisation) or as a default file.  The login message is the (sometimes)
informative message regarding the reason (apart from the initial) for the
login.  Belt-and-braces on the autocomplete of login credential fields; use the
non-standard but widely supported AUTOCOMPLETE="off", as well as a little
JavaScript to empty those fields for agents not supporting the former method.
*/ 

void LoginPage
(
REQUEST_DATA *rdptr,
char *LoginMessage,
char *PostBufferPtr,
int PostBufferCount
)
{
   static $DESCRIPTOR (LoginFaoDsc, "!#XL\0!ACL!UL\0!AZ\0");

   BOOL  LoginChange;
   int  cnt, status,
        CookieContentLength,
        LoginCount,
        LoginPageLength,
        SoyMailHistory;
   unsigned short  slen;
   unsigned long  BinTime [2];
   char  *cptr, *sptr, *zptr, *fnptr,
         *CookieContentPtr,
         *IdChangePtr,
         *IdMessagePtr,
         *LoginPagePtr,
         *PeriodPtr,
         *TitlePtr,
         *UserNamePtr;
   char  LoginBuffer [128];
   $DESCRIPTOR (LoginBufferDsc, LoginBuffer);

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

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

   /* load it if we need it */
   if (*LangFor("lang_name") == '?') LangLoad (SoyMailConfig.LoginLanguage);

   LoginCount = 1;
   UserNamePtr = "";

   LoginChange = FALSE;
   /* if there is evidence of the change password page */
   CGIVARNULL (cptr, "FORM_PASSWORD_CURRENT");
   if (cptr) LoginChange = TRUE;
   if (!LoginChange)
   {
      /* if the last message was one about password expiry */
      if (!strcmp (LoginMessage, SysGetMsg(ACME$_PWDEXPIRED)))
      {
         LoginChange = TRUE;
#if !LOGIN_ACME 
         if (!strncmp (LoginMessage, "Message number", 14))
            LoginMessage = "Password has expired";
#endif
      }
   }
   if (!LoginChange)
   {
      /* if the '[x] Change Password' check box was checked */
      if (!strcmp (LoginMessage, "!change")) LoginChange = TRUE;
   }
   if (LoginChange)
   {
      /* all amounts to naught if the configuration directive is not set */
      if (!SoyMailConfig.LoginChangePage) LoginChange = FALSE;
   }

   /******************/
   /* load HTML file */
   /******************/

   if (LoginChange)
      fnptr = SoyMailConfig.LoginChangePage;
   else
   if (SoyMailConfig.LoginPage)
      fnptr = SoyMailConfig.LoginPage;
   else
      fnptr = LOGIN_PAGE_DEFAULT;
   status = ReadFileIntoMemory (fnptr, &LoginPagePtr, &LoginPageLength);
   if (VMSnok(status)) ErrorExit (status, FI_LI);

   if (!(IdMessagePtr = strstr (LoginPagePtr, "id=\"login_message\">")) &&
       !(IdMessagePtr = strstr (LoginPagePtr, "ID=\"login_message\">")))
   {
      CgiLibResponseError (FI_LI, 0, "Formatting error in %s", fnptr);
      return;
   }
   IdMessagePtr += sizeof("id=\"login_message\">") - 1;
   if (!LoginChange)
   {
      if (!(IdChangePtr = strstr (IdMessagePtr, "id=\"login_change\">")) &&
          !(IdChangePtr = strstr (IdMessagePtr, "ID=\"login_change\">")))
      {
         CgiLibResponseError (FI_LI, 0, "Formatting error in %s", fnptr);
         return;
      }
      IdChangePtr += sizeof("id=\"login_change\">") - 1;
   }
   else
      IdChangePtr = NULL;

   /******************/
   /* process cookie */
   /******************/

   if (LoginGetCookie (rdptr, &CookieContentPtr, &CookieContentLength))
   {
      /* scan across the hexadecimal obfuscator */
      for (cptr = CookieContentPtr; *cptr; cptr++);
      if (*(cptr+1)) cptr++;
      /* verify the cookie structure version indicator */
      if (*cptr == LoginCookieVersion[1]) cptr++; else cptr = "";
      if (*cptr == 'C')
      {
         cptr++;
         /* scan across idle, verify and IP address to username */
         for (cnt = 0; cnt < 3; cnt++)
         {
            while (*cptr) cptr++;
            if (*(cptr+1)) cptr++;
         }
         UserNamePtr = cptr;
      }
      else
      if (*cptr == 'L')
      {
         cptr++;
         LoginCount = atoi(cptr) + 1;
         while (*cptr && isdigit(*cptr)) cptr++;
         if (!*cptr && *(cptr+1)) cptr++;
         UserNamePtr = cptr;
      }
   }

   /* the hexadecimal number just adds a little obfuscation to the mix */
   sys$gettim (&BinTime);
   status = sys$fao (&LoginFaoDsc, &slen, &LoginBufferDsc,
                     (BinTime[0]&7)+1, BinTime[0], LoginCookieVersion,
                     LoginCount, UserNamePtr);
   if (VMSnok(status)) ErrorExit (status, FI_LI);

   LoginSetCookie (rdptr, LoginBuffer, slen);

   /*****************/
   /* process login */
   /*****************/

   /* keeps track of previous soyMAIL usage history for (re-)login purposes */
   CGIVAR (cptr, "FORM_SOYMAIL_HISTORY");
   SoyMailHistory = atoi(cptr);

   TitlePtr = SoyMailPageTitle(rdptr);

   /* this form variable indicates it's from the login page */
   CGIVARNULL (cptr, "FORM_LOGIN_PAGE");
   if (!CookieContentLength && (SoyMailHistory || cptr))
      LoginMessage = LangFor("login_cookie_absent");
   else
   if (!(LoginCount % 10))
      LoginMessage = LangFor("login_failure_count");
   if (LoginMessage[0] == '!') LoginMessage = "";
   /* OCD */
   for (cptr = LoginMessage; *cptr; cptr++);
   if (cptr > LoginMessage) cptr--;
   if (*cptr && !ispunct(*cptr)) PeriodPtr = "."; else PeriodPtr = "";

   if (LoginMessage[0]) TitlePtr = LoginMessage;

   zptr = (sptr = rdptr->FormAction) + sizeof(rdptr->FormAction)-1;

   if (strsame (rdptr->CgiRequestUriPtr,
                rdptr->CgiScriptNamePtr,
                strlen(rdptr->CgiScriptNamePtr)))
   {
      cptr = rdptr->CgiScriptNamePtr;
      while (*cptr && sptr < zptr) *sptr++ = *cptr++;
      if (sptr < zptr) *sptr++ = '/';
      if (!SoyMailConfig.PrivateRequest && sptr < zptr) *sptr++ = '~';
   }
   else
   {
      cptr = rdptr->CgiRequestUriPtr;
      if (*(USHORTPTR)cptr == '/\0') cptr = "";
      while (*cptr && sptr < zptr) *sptr++ = *cptr++;
   }
   if (sptr >= zptr) ErrorExit (SS$_BUGCHECK, FI_LI);
   *sptr = '\0';

   /*********************/
   /* output login page */
   /*********************/

   /*
      Try to ensure the login page is not cached!
      If it is cached and the login page is not freshly retrieved from
      the server the initial (empty) login cookie will not be present.
   */
   CgiLibResponseHeader (200, "text/html", "%s%s%s",
                         "Pragma: no-cache\n",
                         "Cache-Control: no-cache, no-store, private\n",
                         rdptr->LoginSetCookiePtr);

   /* if we're playing silly-buggers */
   if (LoginCount > 10)
   {
      if (WatchEnabled)
         WatchThis (FI_LI, "FAILURES !UL sleep(!UL) seconds",
                    LoginCount, LoginCount/5);
      sleep (LoginCount / 5);
   }

   /* no DOCTYPE, back to quirks mode, to retain compatible page layout */
   fprintf (stdout,
"<html>\n\
<head>\n\
<title>%s</title>\n\
</head>\n\
<body onload=\"whenLoaded()\">\n\
<form \
action=\"%s\" \
method=\"POST\" \
accept-charset=\"%s\" \
enctype=\"multipart/form-data\"%s>\n\
<input type=\"hidden\" name=\"soymail_history\" value=\"%d\">\n\
<input type=\"hidden\" name=\"login_page\" value=\"\">\n",
            TitlePtr,
            rdptr->CgiRequestUriPtr,
            LangFor("option_soy_charset_default"),
            (!LoginChange && SoyMailConfig.LoginAutoComplete) ?
               "" : " AUTOCOMPLETE=\"off\"",
            SoyMailHistory);

   if (LoginChange)
   {
      /* if from a previous login page */
      CGIVARNULL (cptr, "FORM_LOGIN_USERNAME");
      if (cptr)
         fprintf (stdout,
"<input type=\"hidden\" name=\"login_username\" value=\"%s\">\n",
                  cptr);
   }

   CGIVARNULL (cptr, "FORM_LOGIN_BODY");
   if (cptr)
   {
      /* propagated from immediately preceding login page */
      CGIVARNULL (sptr, "FORM_LOGIN_TYPE");
      if (!sptr) ErrorExit (SS$_BUGCHECK, FI_LI);
   }
   else
   {
      /* from [logout] button or GET request */
      cptr = PostBufferPtr;
      if (cptr) CGIVARNULL (sptr, "CONTENT_TYPE");
   }
   if (cptr)
   {
      /* save the originating request body */
      fprintf (stdout,
"<input type=\"hidden\" name=\"login_type\" value=\"%s\">\n\
<input type=\"hidden\" name=\"login_body\" value=\"%s\">\n",
               (char*)CgiLibHtmlEscape (sptr, -1, NULL, 0),
               CerealDataOut (cptr, PostBufferCount));
   }

   fprintf (stdout,
"<script language=\"JavaScript\">\n\
function whenLoaded () { ");

   if (LoginChange)
   {
      fprintf (stdout,
"document.getElementById(\'password_current\').value=\
document.getElementById(\'password_new\').value=\
document.getElementById(\'password_confirm\').value=\'\'; \
document.getElementById(\'password_current\').focus(); \
return true; }\n\
function resetClick () \
{ document.getElementById(\'password_current\').focus(); return true; }\n");
   }
   else
   {
      if (!SoyMailConfig.LoginAutoComplete)
         fprintf (stdout,
"document.getElementById(\'login_username\').value=\
document.getElementById(\'login_password\').value=\'\'; ");

      fprintf (stdout,
"document.getElementById(\'login_username\').focus(); return true; }\n\
function resetClick () \
{ document.getElementById(\'login_username\').focus(); return true; }\n");
   }

   fprintf (stdout,
"function submitClick () { return true; }\n\
</script>\n");

   /* output up to and including any login message */
   fprintf (stdout, "%*.*s", IdMessagePtr-LoginPagePtr,
                             IdMessagePtr-LoginPagePtr,
                             LoginPagePtr);
   if (LoginMessage[0]) fprintf (stdout, "%s%s", LoginMessage, PeriodPtr);

   if (IdChangePtr)
   {
      /* output up to any change checkbox */
      fprintf (stdout, "%*.*s", IdChangePtr-IdMessagePtr,
                                IdChangePtr-IdMessagePtr,
                                IdMessagePtr);
      if (!SoyMailConfig.LoginChangePage)
      {
         /* when it's not configured absorb the checkbox */
         while (*IdChangePtr &&
                (*IdChangePtr != '<' || strncmp(IdChangePtr,"</tr>",5)))
            IdChangePtr++;
      }
      /* output what's following the checkbox */
      fputs (IdChangePtr, stdout);
   }
   else
   {
      /* just the rest of the (login or change) page HTML */
      fputs (IdMessagePtr, stdout);
   }

   fprintf (stdout,
"</form>\n\
</body>\n\
</html>\n");
}

/*****************************************************************************/
/*
Get the content of the HTTP_COOKIE CGI variable if present.  Search for the
soyMAIL login credentials cookie name and associated value.  If found parse out
the value, decode the base-64 and then decrypt the content.  Store that plain
content in a static buffer, allocate a dynamic string and return the location
of that and it's length in the supplied parameters.  Return the length of
cookie or zero if there is none (or an error processing it).
*/ 

int LoginGetCookie
(
REQUEST_DATA *rdptr,
char **CookieContentPtr,
int *CookieLengthPtr
)
{
   static int  CookieLength;
   static char  CookieBuffer [256];

   int  len;
   char  *cptr, *sptr, *zptr;
   rc4_key  rc4Key;

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

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

   if (!SoyMailConfig.LoginSecret) ErrorExit (SS$_BUGCHECK, FI_LI);

   *CookieContentPtr = NULL;
   *CookieLengthPtr = 0;

   CGIVARNULL (cptr, "HTTP_COOKIE");
   if (!cptr) return (0);

   if (rdptr->PublicAuthorized)
   {
      sptr = LoginPublicCookieName;
      len = sizeof(LoginPublicCookieName)-1;
   }
   else
   {
      sptr = LoginPrivateCookieName;
      len = sizeof(LoginPrivateCookieName)-1;
   }
   cptr = strstr (cptr, sptr);
   if (!cptr) return (0);
   cptr += len;
   if (*cptr != '=') return (0);

   /* found what looks like it, copy into the credentials buffer */
   cptr++;
   zptr = (sptr = CookieBuffer) + sizeof(CookieBuffer)-1;
   while (*cptr && *cptr != ';' && *cptr != ' ' && sptr < zptr)
      *sptr++ = *cptr++;
   if (sptr >= zptr) ErrorExit (SS$_BUGCHECK, FI_LI);
   *sptr = '\0';
   if (WatchEnabled)
      WatchThis (FI_LI, "!AZ \"!AZ\"",
                 rdptr->PublicAuthorized ? LoginPublicCookieName :
                                           LoginPrivateCookieName,
                 CookieBuffer);

   /* decode from base64 to encrypted bytes */
   if ((CookieLength = MimeDecBase64 (CookieBuffer)) <= 0)
   {
      if (WatchEnabled) WatchThis (FI_LI, "BASE64 decode failed");
      return (CookieLength = 0);
   }

   /* decrypt the RC4 encrypted credentials to plain bytes */
   rc4_prepare_key ((unsigned char*)SoyMailConfig.LoginSecret,
                    strlen(SoyMailConfig.LoginSecret),
                    &rc4Key);
   rc4_crypt ((unsigned char*)CookieBuffer, CookieLength, &rc4Key);

#if LOGIN_WATCH_CRED
   if (WatchEnabled)
   {
      WatchThis (FI_LI, "COOKIE");
      WatchDump (CookieBuffer, CookieLength);
   }
#endif

   cptr =  CgiLibVeeMemCalloc (CookieLength);
   memcpy (cptr, CookieBuffer, CookieLength);

   *CookieContentPtr = cptr;
   *CookieLengthPtr = CookieLength;

   return (CookieLength);
}

/*****************************************************************************/
/*
Create an field suitable for inclusion in an HTTP response to set the supplied
cookie value.  The cookie value is RC4 encrypted and the base-64 encoded into
text suitable for use as a cookie value.  No expiry is set so the cookie should
disappear with the close of the browser.  A path and domain is supplied so the
cookie should only be be sent to soyMAIL.
*/ 

void LoginSetCookie
(
REQUEST_DATA *rdptr,
char *CookieContent,
int CookieLength
)
{
   static $DESCRIPTOR (LoginSetCookieFaoDsc,
"Set-Cookie: !AZ=!AZ; path=!AZ; domain=!AZ; SameSite=Strict;\n\0");

   int  status,
        SetCookieLength;
   unsigned short  slen;
   char  *cptr, *sptr, *zptr,
         *Base64Ptr,
         *PathPtr;
   char  ContentBuffer [256],
         DomainBuffer [128],
         SetCookieBuffer [256];
   $DESCRIPTOR (SetCookieBufferDsc, SetCookieBuffer);
   rc4_key  rc4Key;

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

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

   if (!SoyMailConfig.LoginSecret) ErrorExit (SS$_BUGCHECK, FI_LI);

#if LOGIN_WATCH_CRED
   if (WatchEnabled)
   {
      WatchThis (FI_LI, "COOKIE");
      WatchDump (CookieContent, CookieLength);
   }
#endif

   if (CookieLength > sizeof(ContentBuffer)) ErrorExit (SS$_BUGCHECK, FI_LI);
   memcpy (ContentBuffer, CookieContent, CookieLength);

   /* encrypt the plain credentials using RC4 */
   rc4_prepare_key ((unsigned char*)SoyMailConfig.LoginSecret,
                    strlen(SoyMailConfig.LoginSecret),
                    &rc4Key);
   rc4_crypt ((unsigned char*)ContentBuffer, CookieLength, &rc4Key);

   if (WatchEnabled) WatchDump (ContentBuffer, CookieLength);

   Base64Ptr = MimeEncBase64 (ContentBuffer, CookieLength, "");
   if (!Base64Ptr) ErrorExit (SS$_BUGCHECK, FI_LI);

   CGIVARNULL (cptr, "HTTP_HOST");
   if (!cptr || !*cptr) CGIVAR (cptr, "SERVER_NAME");
   if (!cptr || !*cptr) ErrorExit (SS$_BUGCHECK, FI_LI);
   /* need to remove any potential non-well-known :port */
   zptr = (sptr = DomainBuffer) + sizeof(DomainBuffer)-1;
   while (*cptr && *cptr != ':' && sptr < zptr) *sptr++ = *cptr++;
   *sptr = '\0';

   if (strsame (rdptr->CgiScriptNamePtr,
                rdptr->CgiRequestUriPtr,
                strlen(rdptr->CgiScriptNamePtr)))
      PathPtr = rdptr->CgiScriptNamePtr;
   else
      PathPtr = "/";

   status = sys$fao (&LoginSetCookieFaoDsc, &slen, &SetCookieBufferDsc,
                     rdptr->PublicAuthorized ? LoginPublicCookieName :
                                               LoginPrivateCookieName,
                     Base64Ptr, PathPtr, DomainBuffer);
   if (VMSnok(status)) ErrorExit (status, FI_LI);
   SetCookieLength = slen;

   rdptr->LoginSetCookiePtr =  CgiLibVeeMemCalloc (slen+1);
   strcpy (rdptr->LoginSetCookiePtr, SetCookieBuffer);
}

/*****************************************************************************/
/*
All requests subjected to soyMAIL authentication are validated through this
function.  It verifies the user credentials, either as supplied by cookie
value or from LoginPage() form fields, and returns a string containing the
failure message if invalid, or NULL if valid.
*/ 

char* LoginVerifyUser (REQUEST_DATA *rdptr)

{
#define CLI$_INSFPRM 229448
#define CLI$_PWDNOTVER 231562

   static $DESCRIPTOR (CredFaoDsc, "!#XL\0!ACC!UL\0!UL\0!AZ\0!AZ\0!AZ\0!AZ\0");

   int  status,
        CredLength,
        CookieContentLength,
        LoginCount,
        SetCookieLength;
   unsigned long  IdleSeconds,
                  LoginIdleSeconds,
                  LoginVerifySeconds,
                  TimeSeconds,
                  VerifySeconds;
   unsigned long  BinTime [2];
   unsigned short  slen;
   char  *cptr, *sptr, *zptr,
         *CookieContentPtr,
         *CookieUserNamePtr,
         *IpAddrPtr,
         *LoginAliasPtr,
         *PasswordPtr,
         *PasswordConfirmPtr,
         *PasswordNewPtr,
         *RemoteAddrPtr,
         *UserNamePtr;
   char  CredBuffer [256];
   $DESCRIPTOR (CredBufferDsc, CredBuffer);

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

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

   if (!SoyMailConfig.LoginSecret) ErrorExit (SS$_BUGCHECK, FI_LI);

   /* load it if we need it */
   if (*LangFor("lang_name") == '?') LangLoad (SoyMailConfig.LoginLanguage);

   if (!(LoginIdleSeconds = SoyMailConfig.LoginIdleSeconds))
      LoginIdleSeconds = LOGIN_IDLE_SECONDS_DEFAULT;
   if (!(LoginVerifySeconds = SoyMailConfig.LoginVerifySeconds))
      LoginVerifySeconds = LOGIN_VERIFY_SECONDS_DEFAULT;

   CGIVARNULL (RemoteAddrPtr, "REMOTE_ADDR");
   if (!RemoteAddrPtr || !*RemoteAddrPtr) ErrorExit (SS$_BUGCHECK, FI_LI);

   time (&TimeSeconds);

   /* initialise global values */
   rdptr->RemoteUser[0] = '\0';
   rdptr->RemoteUserLength = 0;

   /* these will be used to detect a password change request */
   PasswordNewPtr = PasswordConfirmPtr = NULL;

   if (LoginGetCookie (rdptr, &CookieContentPtr, &CookieContentLength))
   {
      /* scan across the hexadecimal obfuscation */
      for (cptr = CookieContentPtr; *cptr; cptr++);
      if (*(cptr+1)) cptr++;
      /* verify the cookie structure version indicator */
      if (*cptr == LoginCookieVersion[1]) cptr++; else cptr = "";
   }
   else
   {
      /* cookie does not exist */
      return ("");
   }

   if (*cptr == 'L')
   {
      /*********/
      /* login */
      /*********/

      CGIVARNULL (UserNamePtr, "FORM_LOGIN_USERNAME");
      if (!UserNamePtr) return ("");

      IdleSeconds = VerifySeconds = 0;
      for (cptr = UserNamePtr; isspace(*cptr); cptr++);
      if (!*cptr) return (LangFor("login_credentials_incomplete"));

      /* if the checkbox '[x] Change Password' */
      CGIVARNULL (cptr, "FORM_LOGIN_CHANGE");
      if (cptr) return ("!change");

      CGIVARNULL (PasswordPtr, "FORM_LOGIN_PASSWORD");
      if (PasswordPtr)
      {
         /*******************/
         /* from login page */
         /*******************/

         RequestAccessLog (NULL, "login");
         for (cptr = PasswordPtr; isspace(*cptr); cptr++);
         if (!*cptr) return (LangFor("login_credentials_incomplete"));
         PasswordNewPtr = PasswordConfirmPtr = NULL;
      }
      else
      {
         /*****************************/
         /* from password change page */
         /*****************************/

         RequestAccessLog (NULL, "change");

         CGIVARNULL (PasswordPtr, "FORM_PASSWORD_CURRENT");
         if (!PasswordPtr) ErrorExit (SS$_BUGCHECK, FI_LI);
         for (cptr = PasswordPtr; isspace(*cptr); cptr++);
         if (!*cptr) return (LangFor("login_credentials_incomplete"));

         CGIVARNULL (PasswordNewPtr, "FORM_PASSWORD_NEW");
         if (!PasswordNewPtr) ErrorExit (SS$_BUGCHECK, FI_LI);
         for (cptr = PasswordNewPtr; isspace(*cptr); cptr++);
         if (!*cptr) return (LangFor("login_credentials_incomplete"));

         CGIVARNULL (PasswordConfirmPtr, "FORM_PASSWORD_CONFIRM");
         if (!PasswordConfirmPtr) ErrorExit (SS$_BUGCHECK, FI_LI);
         for (cptr = PasswordConfirmPtr; isspace(*cptr); cptr++);
         if (!*cptr) return (LangFor("login_credentials_incomplete"));

         /* verify the new and confirmation passwords are identical */
         if (strcmp (PasswordNewPtr, PasswordConfirmPtr))
            return (SysGetMsg(CLI$_PWDNOTVER));

         /* verify it's a different password */
         if (!strcmp (PasswordPtr, PasswordNewPtr))
            return (SysGetMsg(SS$_PWDNOTDIF));
      }

      if (!LoginAlias (&UserNamePtr, &LoginAliasPtr))
         return (LangFor("login_authentication_failure"));
   }
   else
   if (*cptr == 'C')
   {
      /***************/
      /* credentials */
      /***************/

      cptr++;

      /* convert then parse idle time value */
      IdleSeconds = atol(cptr);
      while (*cptr && isdigit(*cptr)) cptr++;
      if (!*cptr && *(cptr+1)) cptr++;

      /* convert then parse verify time value */
      VerifySeconds = atol(cptr);
      while (*cptr && isdigit(*cptr)) cptr++;
      if (!*cptr && *(cptr+1)) cptr++;

      /* point at then parse the IP address */
      for (IpAddrPtr = cptr; *cptr; cptr++);
      if (*(cptr+1)) cptr++;

      /* point at then parse the login alias */
      for (LoginAliasPtr = cptr; *cptr; cptr++);
      if (*(cptr+1)) cptr++;
      /* credential fields are not allowed to be empty */
      if (*LoginAliasPtr == '*') LoginAliasPtr = "";

      /* point at then parse the username */
      for (UserNamePtr = cptr; *cptr; cptr++);
      if (*(cptr+1)) cptr++;

      /* point at the password */
      PasswordPtr = cptr;

      /****************/
      /* sanity check */
      /****************/

      if (!IdleSeconds || !VerifySeconds ||
          !*IpAddrPtr || !*UserNamePtr || !*PasswordPtr)
      {
         if (WatchEnabled) WatchThis (FI_LI, "SANITY check failure (secret changed?)");
         return (LangFor("login_credentials_invalid"));
      }

      /*********************/
      /* verify IP address */
      /*********************/

      /* if the remote IP address does not match that in the credentials */
      if (strcmp (IpAddrPtr, RemoteAddrPtr))
      {
         if (WatchEnabled)
            WatchThis (FI_LI, "IP mismatch !AZ and !AZ", IpAddrPtr, RemoteAddrPtr);
         return (LangFor("login_credentials_invalid"));
      }

      /*****************/
      /* idle timeout? */
      /*****************/

      /* if we've not reached the timestamp to re-verify */
      if (WatchEnabled)
         WatchThis (FI_LI, "IDLE !UL < !UL !AZ",
                    TimeSeconds, IdleSeconds,
                    IdleSeconds < TimeSeconds ? "TRUE" : "FALSE");

      /* if yes then it's been idle for too long! */
      if (IdleSeconds < TimeSeconds) return (LangFor("login_idle_session"));

      /*******************/
      /* verify timeout? */
      /*******************/

      /* if we've not reached the timestamp to re-verify */
      if (WatchEnabled)
         WatchThis (FI_LI, "VERIFY !UL < !UL !AZ",
                    TimeSeconds, VerifySeconds,
                    VerifySeconds < TimeSeconds ? "TRUE" : "FALSE");
   }
   else
   {
      /********/
      /* hmmm */
      /********/

      if (WatchEnabled) WatchThis (FI_LI, "SANITY check failure");
      return (LangFor("login_credentials_invalid"));
   }

   /* for first access and subsequent verify timeouts */
   if (VerifySeconds < TimeSeconds)
   {
      if (PasswordNewPtr && PasswordConfirmPtr)
      {
         /*******************/
         /* change password */
         /*******************/

#if LOGIN_ACME 
         status = LoginAcmeVerifyUser (UserNamePtr, PasswordPtr,
                                       PasswordNewPtr, RemoteAddrPtr);
#else
         status = LoginSysUafVerifyUser (UserNamePtr, PasswordPtr,
                                         PasswordNewPtr, RemoteAddrPtr);
#endif
      }
      else
      {
         /*******************/
         /* verify password */
         /*******************/

         if (SoyMailConfig.LoginAgent)
         {
            /* via agent in spawned process */
            status = LoginAgentVerifyUser (UserNamePtr, PasswordPtr,
                                           RemoteAddrPtr);
         }
         else
         {
            /* verified against VMS credentials */
#if LOGIN_ACME 
            status = LoginAcmeVerifyUser (UserNamePtr, PasswordPtr,
                                          NULL, RemoteAddrPtr);
#else
            status = LoginSysUafVerifyUser (UserNamePtr, PasswordPtr,
                                            NULL, RemoteAddrPtr);
#endif
         }
      }

      if (VMSnok (status))
      {
         /*************************/
         /* authentication failed */
         /*************************/

         if (status == CLI$_INSFPRM)
            return (LangFor("login_credentials_incomplete"));
         else
         if (status != SS$_INVLOGIN)
            return (SysGetMsg(status));
         else
            return (LangFor("login_authentication_failure"));
      }

      /* set new verification period */
      VerifySeconds = TimeSeconds + LoginVerifySeconds;
   }

   /* set new idle period */
   IdleSeconds = TimeSeconds + LoginIdleSeconds;

   /*******************/
   /* set credentials */
   /*******************/

   zptr = (sptr = rdptr->LoginAlias) + sizeof(rdptr->LoginAlias)-1;
   for (cptr = LoginAliasPtr; *cptr && sptr < zptr; *sptr++ = *cptr++);
   if (sptr >= zptr) ErrorExit (SS$_BUGCHECK, FI_LI);
   *sptr = '\0';
   rdptr->LoginAliasLength = sptr - rdptr->LoginAlias;

   zptr = (sptr = rdptr->RemoteUser) + sizeof(rdptr->RemoteUser)-1;
   for (cptr = UserNamePtr; *cptr && sptr < zptr; *sptr++ = *cptr++);
   if (sptr >= zptr) ErrorExit (SS$_BUGCHECK, FI_LI);
   *sptr = '\0';
   rdptr->RemoteUserLength = sptr - rdptr->RemoteUser;

   /* the hexadecimal number just adds a little obfuscation to the mix */
   sys$gettim (&BinTime);
   sys$fao (&CredFaoDsc, &slen, &CredBufferDsc,
            (BinTime[0]&7)+1, BinTime[0], LoginCookieVersion,
            IdleSeconds, VerifySeconds, RemoteAddrPtr,
            /* credential fields are not allowed to be empty */
            *LoginAliasPtr ? LoginAliasPtr : "*",
            UserNamePtr, PasswordPtr);
   CredLength = slen;

   LoginSetCookie (rdptr, CredBuffer, CredLength);

   /* discard output (needed some arbitrary success status) value is 1545 */
   if (status == SS$_CONTROLO) return (SoyMailConfig.PublicNoAccessPage);

   return (NULL);
}

/*****************************************************************************/
#ifdef COMMENTS_WITH_COMMENTS
/*
Map an alias login name to a user name to be used for authentication.  Modify
the alias name pointer to an empty string if no mapping was found.  Return TRUE
when the user name pointer is valid for authentication (mapped or not).  If the
directive "*/-" is encountered return FALSE to indicate that no match means no
authentication.  If the directive "*/*" return TRUE.
*/ 
#endif /* COMMENTS_WITH_COMMENTS */

BOOL LoginAlias
(
char **UserNamePtrPtr,
char **AliasNamePtrPtr
)
{
   static char  AliasName [128],
                UserName [USERNAME_MAX+1];

   char  *aptr, *cptr, *sptr, *zptr;

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

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

   /* an empty string indicates there was no mapping */
   *AliasNamePtrPtr = "";

   if (!(aptr = SoyMailConfig.LoginAlias)) return (TRUE);

   while (*aptr)
   {
      while (*aptr && isspace(*aptr)) aptr++;
      if (!*aptr) break;
      if (*(USHORTPTR)aptr == '*/')
      {
         if (WatchEnabled) WatchThis (FI_LI, "ALIAS !3AZ", aptr);
         if (*(aptr+2) == '*') return (TRUE);
         return (FALSE);
      }
      sptr = *UserNamePtrPtr;
      cptr = aptr;
      while (*aptr && *aptr != '/' && *sptr &&
             tolower(*aptr) == tolower(*sptr))
      {
         aptr++;
         sptr++;
      }
      if (*aptr == '/' && !*sptr)
      {
         /* buffer the in-configuration alias name */
         zptr = (sptr = AliasName) + sizeof(AliasName)-1;
         while (*cptr && *cptr != '/' && sptr < zptr) *sptr++ = *cptr++;
         *sptr = '\0';
         /* buffer the in-configuration mapped user name */
         zptr = (sptr = UserName) + sizeof(UserName)-1;
         for (aptr++; *aptr && *aptr != '\n' && sptr < zptr; *sptr++ = *aptr++);
         *sptr = '\0';
         if (WatchEnabled)
            WatchThis (FI_LI, "ALIAS \"!AZ\" to \"!AZ\"", AliasName, UserName);
         *AliasNamePtrPtr = AliasName;
         *UserNamePtrPtr = UserName;
         return (TRUE);
      }
      while (*aptr && *aptr != '\n') aptr++;
   }

   if (WatchEnabled) WatchThis (FI_LI, "ALIAS \"!AZ\" NONE", *UserNamePtrPtr);
   return (TRUE);
}

/*****************************************************************************/
/*
Spawn a subprocess to execute an authentication agent.  The subprocess should
return a success code (any) to indicate valid credentials or an error code
(any) to indicate failure.  See description in module prologue.
*/ 

int LoginAgentVerifyUser
(
char *UserName,
char *Password,
char *RemoteAddr
)
{
   static $DESCRIPTOR (CommandFaoDsc, "!AZ \"!AZ\" \"!AZ\" \"!AZ\"");
   static $DESCRIPTOR (SysOutputDsc, "SYS$OUTPUT:");
   static $DESCRIPTOR (NullDeviceDsc, "NL:");

   int  status;
   unsigned long  ProcessId,
                  CompletionStatus;
   unsigned short  ShortLength;
   char  CommandBuffer [256];
   $DESCRIPTOR (CommandDsc, CommandBuffer);
   struct dsc$descriptor_s  *OutputDscPtr;

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

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

   status = sys$fao (&CommandFaoDsc, &ShortLength, &CommandDsc,
                     SoyMailConfig.LoginAgent, UserName, Password, RemoteAddr); 
   if (VMSnok (status)) return (status);
   if (status == SS$_BUFFEROVF) return (SS$_RESULTOVF);
   CommandBuffer[ShortLength] = '\0';

   if (WatchEnabled)
   {
      fflush (stdout);
      OutputDscPtr = &SysOutputDsc;
   }
   else
      OutputDscPtr = &NullDeviceDsc;

   status = lib$spawn (&CommandDsc, 0, OutputDscPtr, 0, 0,
                       &ProcessId, &CompletionStatus, 0, 0, 0, 0, 0);

   if (VMSnok (status)) return (status);

   if (WatchEnabled)
      WatchThis (FI_LI, "!AZ %X!8XL", SoyMailConfig.LoginAgent, CompletionStatus);

   return (CompletionStatus);
}

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

/*************/
#if LOGIN_ACME 
/*************/

/*****************************************************************************/
/*
Verify username/password using $ACM services.
Change user password after verification (if 'NewPassword' is non-NULL).
*/ 

int LoginAcmeVerifyUser
(
char *UserName,
char *UserPassword,
char *NewPassword,
char *RemoteHostName
)
{
   static unsigned long  DetachMask [2] = { PRV$M_DETACH, 0 };
   static unsigned long  NewPasswordFlag = ACMEPWDFLG$M_PASSWORD_1;

   int  status,
        SetPrvStatus,
        UserNameLength;
   unsigned short  MappingAcmeNameLength,
                   MappedVmsUserNameLength;
   unsigned long  AcmeLogonType;
   char  *cptr, *sptr, *zptr;
   char  MappedVmsUserName [32] = "",
         MappingAcmeName [32+1] = "",
         UserNameBuffer [256];
   ILE3  *itmptr;
   ILE3  AcmItemList [16];

   /* the SYS$ACM() IO status block is a little different to the norm! */
   unsigned long  AcmIOsb [4];

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

   if (WatchEnabled)
      WatchThis (FI_LI, "!AZ ACME \"!AZ\"",
                 NewPassword ? "CHANGE" : "VERIFY", UserName);

   if (SoyMailConfig.LoginType)
      AcmeLogonType = SoyMailConfig.LoginType;
   else
      AcmeLogonType = ACME$K_NETWORK;

   itmptr = AcmItemList;

   if (SoyMailConfig.LoginAcmeDoi)
   {
      itmptr->ile3$w_code = ACME$_TARGET_DOI_NAME;
      itmptr->ile3$w_length = strlen(SoyMailConfig.LoginAcmeDoi);
      itmptr->ile3$ps_bufaddr = SoyMailConfig.LoginAcmeDoi;
      itmptr->ile3$ps_retlen_addr = 0;
      itmptr++;
   }

   /* requires IMPERSONATE (DETACH) privilege on VMS V7.3-1 or earlier */
   itmptr->ile3$w_code = ACME$_LOGON_TYPE;
   itmptr->ile3$w_length = sizeof(AcmeLogonType);
   itmptr->ile3$ps_bufaddr = &AcmeLogonType;
   itmptr->ile3$ps_retlen_addr = 0;
   itmptr++;

   itmptr->ile3$w_code = ACME$_PRINCIPAL_NAME_IN;
   itmptr->ile3$w_length = strlen(UserName);
   itmptr->ile3$ps_bufaddr = UserName;
   itmptr->ile3$ps_retlen_addr = 0;
   itmptr++;

   /* requires IMPERSONATE (DETACH) privilege */
   itmptr->ile3$w_code = ACME$_REMOTE_HOST_NAME;
   itmptr->ile3$w_length = strlen(RemoteHostName);
   itmptr->ile3$ps_bufaddr = RemoteHostName;
   itmptr->ile3$ps_retlen_addr = 0;
   itmptr++;

   /* create a composite string containing the user name */
   zptr = (sptr = UserNameBuffer) + sizeof(UserNameBuffer)-1;
   for (cptr = "soyMAIL:"; *cptr; *sptr++ = *cptr++);
   for (cptr = UserName; *cptr && sptr < zptr; *sptr++ = toupper(*cptr++));
   *sptr = '\0';
   UserNameLength = sptr - UserNameBuffer;

   /* requires IMPERSONATE (DETACH) privilege */
   itmptr->ile3$w_code = ACME$_REMOTE_USERNAME;
   itmptr->ile3$w_length = UserNameLength;
   itmptr->ile3$ps_bufaddr = UserNameBuffer;
   itmptr->ile3$ps_retlen_addr = 0;
   itmptr++;

   itmptr->ile3$w_code = ACME$_PASSWORD_1;
   itmptr->ile3$w_length = strlen(UserPassword);
   itmptr->ile3$ps_bufaddr = UserPassword;
   itmptr->ile3$ps_retlen_addr = 0;
   itmptr++;

   itmptr->ile3$w_code = ACME$_MAPPED_VMS_USERNAME;
   itmptr->ile3$w_length = sizeof(MappedVmsUserName);
   itmptr->ile3$ps_bufaddr = MappedVmsUserName;
   itmptr->ile3$ps_retlen_addr = &MappedVmsUserNameLength;
   itmptr++;

   if (WatchEnabled)
   {
      itmptr->ile3$w_code = ACME$_MAPPING_ACME_NAME;
      itmptr->ile3$w_length = sizeof(MappingAcmeName);
      itmptr->ile3$ps_bufaddr = MappingAcmeName;
      itmptr->ile3$ps_retlen_addr = &MappingAcmeNameLength;
      itmptr++;
   }

   if (NewPassword)
   {
      itmptr->ile3$w_code = ACME$_NEW_PASSWORD_FLAGS;
      itmptr->ile3$w_length = sizeof(NewPasswordFlag);
      itmptr->ile3$ps_bufaddr = &NewPasswordFlag;
      itmptr->ile3$ps_retlen_addr = 0;
      itmptr++;

      itmptr->ile3$w_code = ACME$_NEW_PASSWORD_1;
      itmptr->ile3$w_length = strlen(NewPassword);
      itmptr->ile3$ps_bufaddr = NewPassword;
      itmptr->ile3$ps_retlen_addr = 0;
      itmptr++;
   }

   /* empty item to terminate the list */
   memset (itmptr, 0, sizeof(ILE3));

   /* provide the IMPERSONATE privilege */
   if (VMSnok (SetPrvStatus = sys$setprv (1, &DetachMask, 0, 0)))
      ErrorExit (SetPrvStatus, FI_LI);

   status = sys$acmw (0,
                      NewPassword ?
                         ACME$_FC_CHANGE_PASSWORD :
                         ACME$_FC_AUTHENTICATE_PRINCIPAL,
                      0,
                      &AcmItemList,
                      &AcmIOsb,
                      0,
                      0);

   if (WatchEnabled) WatchThis (FI_LI, "$ACMW %X!8XL", status);
   if (VMSnok (status)) ErrorExit (status, FI_LI);

   if (VMSnok (SetPrvStatus = sys$setprv (0, &DetachMask, 0, 0)))
      ErrorExit (SetPrvStatus, FI_LI);

   if (VMSok (AcmIOsb[0]))
   {
      MappedVmsUserName[MappedVmsUserNameLength] = '\0';
      if (WatchEnabled) MappingAcmeName[MappingAcmeNameLength] = '\0';
   }
   else
      MappingAcmeName[0] = '\0';

   if (WatchEnabled)
   {
      if (VMSok(AcmIOsb[0]) && AcmIOsb[0] == AcmIOsb[1])
         WatchThis (FI_LI, "ACME doi:\"!AZ\" agent:\"!AZ\" mapped:\"!AZ\" %X!8XL",
                    SoyMailConfig.LoginAcmeDoi ?
                       SoyMailConfig.LoginAcmeDoi : "*",
                    MappingAcmeName, MappedVmsUserName, AcmIOsb[0]);
      else
      if (!AcmIOsb[3] && AcmIOsb[0] == AcmIOsb[1])
         WatchThis (FI_LI, "ACME doi:\"!AZ\" sts:%X!8XL",
                    SoyMailConfig.LoginAcmeDoi ?
                       SoyMailConfig.LoginAcmeDoi : "*",
                    AcmIOsb[0]);
      else
         WatchThis (FI_LI, "ACME doi:\"!AZ\" sts:%X!8XL "
                    "sec:%X!8XL id:!UL acme:%X!8XL mapped:\"!AZ\"",
                    SoyMailConfig.LoginAcmeDoi ?
                       SoyMailConfig.LoginAcmeDoi : "*",
                    AcmIOsb[0], AcmIOsb[1],
                    AcmIOsb[2], AcmIOsb[3],
                    MappedVmsUserName);
   }

   if (AcmIOsb[0] == LGI$_RESTRICT)
   {
      /* authentication OK but restriction on login source */
      if (SoyMailConfig.LoginAcmeNoRestrict)
      {
         if (WatchEnabled)
         WatchThis (FI_LI, "LGI$_RESTRICT - not applying source restriction");
         AcmIOsb[0] = SS$_NORMAL;
      }
   }

   if (AcmIOsb[0] == ACME$_PWDEXPIRED)
   {
      /* authentication OK but restriction because of expired password */
      if (SoyMailConfig.LoginNoPwdExp)
      {
         if (WatchEnabled)
            WatchThis (FI_LI, "ACME$_PWDEXPIRED - not applying password expiry");
         AcmIOsb[0] = SS$_NORMAL;
      }
   }

   return (AcmIOsb[0]);
}

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

/*********************/
#else  /* LOGIN_ACME */
/*********************/

/*****************************************************************************/
/*
Verify username/password using $SCAN_INTRUSION and $GETUAI services.
Change user password after verification (if 'NewPassword' is non-NULL).
*/ 

int LoginSysUafVerifyUser
(
char *UserName,
char *UserPassword,
char *NewPassword,
char *RemoteHostName
)
{
/* according to the documentation these can be returned by $SCAN_INTRUSION */
#define SECSRV$_INTRUDER 116299715
#define SECSRV$_SUSPECT 116299723
#define SECSRV$_SERVERNOTACTIVE 116311802
#define SECSRV$_NOSUCHINTRUDER 116311738 /* NOMATCH */
#define SECSRV$_INSUFINFO 116311754

/* just for convenience sake */
#define LOGIN_PRV$M_SECURITY 0x40

   static unsigned long  SecurityMask [2] = { 0, LOGIN_PRV$M_SECURITY };

   int  status,
        GetUaiStatus,
        SetPrvStatus;
   unsigned long  LoginType;
   char  *cptr, *sptr, *zptr;
   char  UserNameBuffer [32+1];
   $DESCRIPTOR (UserNameDsc, UserNameBuffer);
   $DESCRIPTOR (RemoteHostNameDsc, RemoteHostName);

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

   if (WatchEnabled) WatchThis (FI_LI, "VERIFY SYSUAF \"!AZ\"", UserName);

   if (SoyMailConfig.LoginType)
      LoginType = SoyMailConfig.LoginType;
   else
      LoginType = JPI$K_NETWORK;

   /* create a composite string containing the user name */
   zptr = (sptr = UserNameBuffer) + sizeof(UserNameBuffer)-1;
   for (cptr = "soyMAIL:"; *cptr; *sptr++ = *cptr++);
   for (cptr = UserName; *cptr && sptr < zptr; *sptr++ = toupper(*cptr++));
   *sptr = '\0';
   UserNameDsc.dsc$w_length = sptr - UserNameBuffer;

   RemoteHostNameDsc.dsc$a_pointer = RemoteHostName;
   RemoteHostNameDsc.dsc$w_length = strlen(RemoteHostName);

   /* provide the SECURITY privilege */
   if (VMSnok (SetPrvStatus = sys$setprv (1, &SecurityMask, 0, 0)))
      ErrorExit (SetPrvStatus, FI_LI);

   status = sys$scan_intrusion (SS$_NORMAL, &UserNameDsc, LoginType,
                                0, &RemoteHostNameDsc, &UserNameDsc,
                                0, 0, 0, 0, 0);

   if (WatchEnabled) WatchThis (FI_LI, "$SCAN_INTRUSION %X!8XL", status);
   if (VMSnok (status)) ErrorExit (status, FI_LI);

   if (VMSnok (SetPrvStatus = sys$setprv (0, &SecurityMask, 0, 0)))
      ErrorExit (SetPrvStatus, FI_LI);

   if (status == SECSRV$_INTRUDER) return (SS$_INVLOGIN);
   if (status == SECSRV$_SERVERNOTACTIVE) return (SS$_INVLOGIN);
   if (status == SECSRV$_INSUFINFO) return (SS$_INVLOGIN);

   GetUaiStatus = LoginUaiUser (UserName, UserPassword, NewPassword);
   if (WatchEnabled) WatchThis (FI_LI, "%X!8XL", GetUaiStatus);

   if (VMSnok (SetPrvStatus = sys$setprv (1, &SecurityMask, 0, 0)))
      ErrorExit (SetPrvStatus, FI_LI);

   status = sys$scan_intrusion (GetUaiStatus, &UserNameDsc, LoginType,
                                0, &RemoteHostNameDsc, &UserNameDsc,
                                0, 0, 0, 0, 0);

   if (WatchEnabled) WatchThis (FI_LI, "$SCAN_INTRUSION %X!8XL", status);

   if (VMSnok (SetPrvStatus = sys$setprv (0, &SecurityMask, 0, 0)))
      ErrorExit (SetPrvStatus, FI_LI);

   return (GetUaiStatus);
}

/*****************************************************************************/
/*
Use $GETUAI and $HASH_PASSWORD to verify the user's credentials.  Also ensure
the account is not disusered, etc., and that the password(s) have not expired.
*/ 

int LoginUaiUser
(
char *UserName,
char *UserPassword,
char *NewPassword
)
{
   /* UAI flags that disallow access */
   static unsigned long  DisallowFlags =
          UAI$M_DISACNT | UAI$M_PWD_EXPIRED | UAI$M_PWD2_EXPIRED;

   int  status;

   unsigned long  Context,
                  UaiFlags,
                  UaiPrimeDays,
                  UaiNetworkAccessP,
                  UaiNetworkAccessS,
                  UaiRemoteAccessP,
                  UaiRemoteAccessS,
                  UaiUic;

   unsigned long  HashedPwd [2],
                  UaiExpBinTime [2],
                  UaiPwd [2],
                  UaiPwd2 [2],
                  UaiPwdDateBinTime [2],
                  UaiPwd2DateBinTime [2],
                  UaiPwdLifeBinTime [2];

   unsigned short  UaiSalt;

   unsigned char  UaiEncrypt,
                  UaiPwdLength;

   char  *cptr, *sptr, *zptr;

   char  PasswordBuffer [64+1],
         UserNameBuffer [15+1];

   $DESCRIPTOR (PasswordDsc, PasswordBuffer);
   $DESCRIPTOR (UserNameDsc, UserNameBuffer);

   ILE3  *itmptr;
   ILE3  UaiItemList [24];

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

   if (WatchEnabled) WatchThis (FI_LI, "GETUAI \"!AZ\"", UserName);

   zptr = (sptr = UserNameBuffer) + sizeof(UserNameBuffer)-1;
   for (cptr = UserName; *cptr && sptr < zptr; *sptr++ = toupper(*cptr++));
   *sptr = '\0';
   UserNameDsc.dsc$w_length = sptr - UserNameBuffer;

   itmptr = UaiItemList;

   itmptr->ile3$w_length = sizeof(UaiUic);
   itmptr->ile3$w_code = UAI$_UIC;
   itmptr->ile3$ps_bufaddr = &UaiUic;
   itmptr->ile3$ps_retlen_addr = 0;
   itmptr++;

   itmptr->ile3$w_length = sizeof(UaiExpBinTime);
   itmptr->ile3$w_code = UAI$_EXPIRATION;
   itmptr->ile3$ps_bufaddr = &UaiExpBinTime;
   itmptr->ile3$ps_retlen_addr = 0;
   itmptr++;

   itmptr->ile3$w_length = sizeof(UaiPwdDateBinTime);
   itmptr->ile3$w_code = UAI$_PWD_DATE;
   itmptr->ile3$ps_bufaddr = &UaiPwdDateBinTime;
   itmptr->ile3$ps_retlen_addr = 0;
   itmptr++;

   itmptr->ile3$w_length = sizeof(UaiPwd2DateBinTime);
   itmptr->ile3$w_code = UAI$_PWD2_DATE;
   itmptr->ile3$ps_bufaddr = &UaiPwd2DateBinTime;
   itmptr->ile3$ps_retlen_addr = 0;
   itmptr++;

   itmptr->ile3$w_length = sizeof(UaiPwdLifeBinTime);
   itmptr->ile3$w_code = UAI$_PWD_LIFETIME;
   itmptr->ile3$ps_bufaddr = &UaiPwdLifeBinTime;
   itmptr->ile3$ps_retlen_addr = 0;
   itmptr++;

   itmptr->ile3$w_length = sizeof(UaiPwdLength);
   itmptr->ile3$w_code = UAI$_PWD_LENGTH;
   itmptr->ile3$ps_bufaddr = &UaiPwdLength;
   itmptr->ile3$ps_retlen_addr = 0;
   itmptr++;

   itmptr->ile3$w_length = sizeof(UaiFlags);
   itmptr->ile3$w_code = UAI$_FLAGS;
   itmptr->ile3$ps_bufaddr = &UaiFlags;
   itmptr->ile3$ps_retlen_addr = 0;
   itmptr++;

   itmptr->ile3$w_length = sizeof(UaiPwd);
   itmptr->ile3$w_code = UAI$_PWD;
   itmptr->ile3$ps_bufaddr = &UaiPwd;
   itmptr->ile3$ps_retlen_addr = 0;
   itmptr++;

   itmptr->ile3$w_length = sizeof(UaiPwd2);
   itmptr->ile3$w_code = UAI$_PWD2;
   itmptr->ile3$ps_bufaddr = &UaiPwd2;
   itmptr->ile3$ps_retlen_addr = 0;
   itmptr++;

   itmptr->ile3$w_length = sizeof(UaiEncrypt);
   itmptr->ile3$w_code = UAI$_ENCRYPT;
   itmptr->ile3$ps_bufaddr = &UaiEncrypt;
   itmptr->ile3$ps_retlen_addr = 0;
   itmptr++;

   itmptr->ile3$w_length = sizeof(UaiSalt);
   itmptr->ile3$w_code = UAI$_SALT;
   itmptr->ile3$ps_bufaddr = &UaiSalt;
   itmptr->ile3$ps_retlen_addr = 0;
   itmptr++;

   itmptr->ile3$w_length = sizeof(unsigned long);
   itmptr->ile3$w_code = UAI$_PRIMEDAYS;
   itmptr->ile3$ps_bufaddr = &UaiPrimeDays;
   itmptr->ile3$ps_retlen_addr = 0;
   itmptr++;

   itmptr->ile3$w_length = 3;
   itmptr->ile3$w_code = UAI$_NETWORK_ACCESS_P;
   itmptr->ile3$ps_bufaddr = &UaiNetworkAccessP;
   itmptr->ile3$ps_retlen_addr = 0;
   itmptr++;

   itmptr->ile3$w_length = 3;
   itmptr->ile3$w_code = UAI$_NETWORK_ACCESS_S;
   itmptr->ile3$ps_bufaddr = &UaiNetworkAccessS;
   itmptr->ile3$ps_retlen_addr = 0;
   itmptr++;

   itmptr->ile3$w_length = 3;
   itmptr->ile3$w_code = UAI$_REMOTE_ACCESS_P;
   itmptr->ile3$ps_bufaddr = &UaiRemoteAccessP;
   itmptr->ile3$ps_retlen_addr = 0;
   itmptr++;

   itmptr->ile3$w_length = 3;
   itmptr->ile3$w_code = UAI$_REMOTE_ACCESS_S;
   itmptr->ile3$ps_bufaddr = &UaiRemoteAccessS;
   itmptr->ile3$ps_retlen_addr = 0;
   itmptr++;

   memset (itmptr, 0, sizeof(ILE3));

   Context = -1;
   status = sys$getuai (0, &Context, &UserNameDsc, &UaiItemList, 0, 0, 0);

   if (WatchEnabled) WatchThis (FI_LI, "$GETUAI %X!8XL", status);
   if (VMSnok (status)) return (status);

   if (WatchEnabled)
      WatchThis (FI_LI, "expire:!%D pwd:!%D (!8XL!8XL) pwd2:!%D life:!%D \
flags:!8XL prime:!8XL netp:!6XL nets:!6XL remp:!6XL rems:!6XL",
            &UaiExpBinTime,
            &UaiPwdDateBinTime,
            UaiPwdDateBinTime[0], UaiPwdDateBinTime[1],
            &UaiPwd2DateBinTime,
            &UaiPwdLifeBinTime,
            UaiFlags,
            UaiPrimeDays,
            UaiNetworkAccessP, UaiNetworkAccessS,
            UaiRemoteAccessP, UaiRemoteAccessS);

   zptr = (sptr = PasswordBuffer) + sizeof(PasswordBuffer)-1;
   if (UaiFlags & UAI$M_PWDMIX)
   {
      /* 7.3-2 mixed-case plus printable char passwords */
      for (cptr = UserPassword; *cptr && sptr < zptr; *sptr++ = *cptr++);
   }
   else
   {
      /* to uppercase! */
      for (cptr = UserPassword;
           *cptr && sptr < zptr;
           *sptr++ = toupper(*cptr++));
   }
   *sptr = '\0';
   PasswordDsc.dsc$w_length = sptr - PasswordBuffer;

   status = sys$hash_password (&PasswordDsc, UaiEncrypt,
                               UaiSalt, &UserNameDsc, &HashedPwd);
   if (VMSnok (status)) 
   {
      if (WatchEnabled) WatchThis (FI_LI, "$HASH_PASSWORD %X!8XL", status);
      return (status);
   }

   if (HashedPwd[0] != UaiPwd[0] ||
       HashedPwd[1] != UaiPwd[1])
   {
      if (WatchEnabled) WatchThis (FI_LI, "PASSWORD match failed");
      return (SS$_INVLOGIN);
   }

   /* automatically disallow if any of these flags are set! */
   if (UaiFlags & DisallowFlags)
   {
      if (WatchEnabled)
      {
         if (UaiFlags & UAI$M_DISACNT)
            WatchThis (FI_LI, "FLAG failure DISACNT");
         else
         if (UaiFlags & UAI$M_PWD_EXPIRED)
            WatchThis (FI_LI, "FLAG failure PWD_EXPIRED");
         else
         if (UaiFlags & UAI$M_PWD2_EXPIRED)
            WatchThis (FI_LI, "FLAG failure PWD2_EXPIRED");
         else
            WatchThis (FI_LI, "FLAG failure ?");
      }
      return (SS$_INVLOGIN);
   }

   if (UaiFlags & UAI$M_PWD_EXPIRED ||
       UaiFlags & UAI$M_PWD2_EXPIRED ||
       /* i.e. pre-expired */
       (UaiPwdDateBinTime[0] == 0xffffffff &&
        UaiPwdDateBinTime[1] == 0xffffffff))
   {
      if (WatchEnabled)
         if (SoyMailConfig.LoginNoPwdExp)
            WatchThis (FI_LI, "NOT applying password expiry");
      /*
          This will generate a message similar to
            %NONAME-E-NOMSG, Message number 074AA59A
          and so still be recognisable by LoginPage().
      */
      if (!NewPassword && !SoyMailConfig.LoginNoPwdExp)
         return (ACME$_PWDEXPIRED);
   }

   if (!NewPassword) return (SS$_NORMAL);

   /*******************/
   /* change password */
   /*******************/

   for (cptr = NewPassword; *cptr; cptr++)
   if (cptr - NewPassword > 32) return (SS$_INVPWDLEN);
   if (cptr - NewPassword < UaiPwdLength) return (SS$_INVPWDLEN);
   /* in the absence of anything else just ensure something reasonable */
   if (!UaiPwdLength && cptr - NewPassword < 6) return (SS$_INVPWDLEN);

   zptr = (sptr = PasswordBuffer) + sizeof(PasswordBuffer)-1;
   if (UaiFlags & UAI$M_PWDMIX)
   {
      /* 7.3-2 mixed-case plus printable char passwords */
      for (cptr = NewPassword; *cptr; cptr++)
         if (!isprint(*cptr) || isspace(*cptr)) break;
      if (*cptr) return (SS$_PWDSYNTAX);
      for (cptr = NewPassword; *cptr && sptr < zptr; *sptr++ = *cptr++);
   }
   else
   {
      /* alphanumerics, underscores and dollars */
      for (cptr = NewPassword; *cptr; cptr++)
         if (!isalnum(*cptr) && *cptr != '_' && *cptr != '$') break;
      if (*cptr) return (SS$_PWDSYNTAX);
      /* to uppercase! */
      for (cptr = NewPassword;
           *cptr && sptr < zptr;
           *sptr++ = toupper(*cptr++));
   }
   *sptr = '\0';
   PasswordDsc.dsc$w_length = sptr - PasswordBuffer;

   status = sys$hash_password (&PasswordDsc, UaiEncrypt,
                               UaiSalt, &UserNameDsc, &UaiPwd);
   if (VMSnok (status)) 
   {
      if (WatchEnabled) WatchThis (FI_LI, "$HASH_PASSWORD %X!8XL", status);
      return (status);
   }

   sys$gettim (&UaiPwdDateBinTime);

   itmptr = UaiItemList;

   itmptr->ile3$w_length = sizeof(UaiPwd);
   itmptr->ile3$w_code = UAI$_PWD;
   itmptr->ile3$ps_bufaddr = &UaiPwd;
   itmptr->ile3$ps_retlen_addr = 0;
   itmptr++;

   itmptr->ile3$w_length = sizeof(UaiPwdDateBinTime);
   itmptr->ile3$w_code = UAI$_PWD_DATE;
   itmptr->ile3$ps_bufaddr = &UaiPwdDateBinTime;
   itmptr->ile3$ps_retlen_addr = 0;
   itmptr++;

   memset (itmptr, 0, sizeof(ILE3));

   Context = -1;
   status = sys$setuai (0, &Context, &UserNameDsc, &UaiItemList, 0, 0, 0);

   if (WatchEnabled) WatchThis (FI_LI, "$SETUAI %X!8XL", status);
   if (VMSnok (status)) return (status);

   return (SS$_NORMAL);
}

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

/**********************/
#endif  /* LOGIN_ACME */
/**********************/

