/*****************************************************************************/
/*
                                  soyMAIL.c

                         ~ Son Of YahMAIL (ta da!) ~


COPYRIGHT
---------
Copyright (C) 2005-2025 Mark G.Daniel

Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License.  You may obtain a copy of the
License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied.  See the License for the
specific language governing permissions and limitations under the License.

soyMAIL may be proctored into existence.

  # WASD_CONFIG_GLOBAL
  [DclScriptProctor]
  1 /cgiplus-bin/soymail /cgiplus-bin/soymail



VERSION HISTORY
---------------
12-JUL-2025  MGD  v2.2.0, tweaks (lots, try $ search *.c "-JUL-2025")
09-FEB-2024  MGD  v2.1.0, option [image-inline] add inline attached image(s)
                          config [image-inline] image delay (mS)
                          DraftSave() subject MimeEncIsoString()
                          bugfix; ComposePageRequest() charset setting
                          bugfix; EscapeForJavaScript() unicode
18-AUG-2023  MGD  v2.0.8, MessageCollapseLineBreaks() reduce '\r\n\r\n' to '\n'
                          ComposePreviewMessage() add font-family, font-size
05-MAR-2023  MGD  v2.0.7, bugfix; MimeDecIsoString() copy intermediate chars
                          TargetBlank() Safari requires ".rel = 'opener'"
13-JAN-2023  MGD  v2.0.6, MimeDecParseBody() allow final multipart to be
                    terminated by the multipart boundary (MIME error but ...)
08-JAN-2023  MGD  v2.0.5, MtaMsgToFile() change in fwrite() behaviour with
                    VSI C V7.4-001 on OpenVMS IA64 V8.4-2L3
                    meaning fwrite("",0,1,fp) fails to write a record
23-JUL-2022  MGD  v2.0.4, ComposeCharsetAlert()
                          rework accept-charset=".."
05-APR-2022  MGD  v2.0.3, PMDF "X-VMS-To:" accomodation
22-APR-2021  MGD  v2.0.2, soyMAILexit() SOYMAIL_OPCOM_EXIT OpcomMessage()
19-FEB-2021  MGD  v2.0.1, bugfix; get it RIGHT! CallMailMessageContextEnd()
11-FEB-2021  MGD  v2.0.0, licensing moved from GPL to Apache-2.0
                  VAX no longer implemented
                  move to native 64 bit data storage
                  soyMAIL may be proctored into activation
                  maintain list of most recent opened and destination folders
                  ComposePreviewMessage() preview a in-composure message
                  CGILIB CgiLib__ApacheSetSockOpt() both VSI and HP(E) symbols
                  folder listing page default charset is UTF-8
                  soyMAIL config [charset-default] allows override of above
                  user option [charset-default] allows override of above
                  UTF-8 is the default for folder, options and contacts pages
                  use CgiLibVeeMemContinue() to detect when a (potentially)
                    huge message fails to allocate memory and handle that
                  update TinyMCE to v4.9.4 (the last of the v4 series)
                  bugfix; DEFAULT_ZONE memory leak from multiple missing
                     CallMailMessageContextEnd() after CallMailMessageContext()
                  bugfix; InetMailParseMimeAttrib() span space after equate
26-JUL-2017  MGD  v1.8.3,
                  push NEWMAIL notification on non-focused soyMAIL
                  update TinyMCE to v4.6.4
04-NOV-2016  MGD  v1.8.2,
                  MimeDecIsoString() ensure it's complete or ignored!
                  standardise "input=file" by jumping though hoops (MSIE)
                  use 'ch' units for compose <textarea> widths
                  add version-dependent query-string to .css and .js resource
                    loads in an effort to make resource updates less painful
                    (works with big 4; Chrome, Firefox, Safari, Edge (MSIE))
                  update TinyMCE to v4.4.3
                  bugfix; MessagePrintView() missing SoyMailJavaScriptStuff()
25-SEP-2015  MGD  v1.8.1,
                  compose Confirm[x] enabling confirm-before-send
                  MimeDecParseBody() relax closing boundary
                  bugfix; PublicPageBegin() FORM_ACCEPT_CHARSET
04-JUN-2014  MGD  v1.8.0,
                  JavaScript is now *essential*
                  integrate TinyMCE 4.n as HTML editor
                  PeriodMunger() for non-ODS-5 (TinyMCE) deployment
                  happy birthday #1 son
                  bugfix; DraftPurge() finalise with mail$mailfile_end()
                  bugfix; CallMailCopyFile() finalise with mail$mailfile_end()
                  bugfix; SoyMailLogo() strsame() instead of !strcmp()
22-JUN-2013  MGD  v1.7.0,
                  Cosmetic changes to the default and some other themes
                    largely dependent on later browsers and CSS3
                  NewMailRefreshXMLHttpRequest() and NewMailXMLHttpRequest()
                    provide a more modern new mail check at the server
                    [check-newmail-ajax] reverts to the older IFRAME check
                  MessagePreview() munge end-of-line into newline so
                    that new CSS can preview as pre-formatted text
                  Un-inline JavaScript code where possible and deploy to
                    SOYMAIL.JS with vars set in-line by <script>..</script>
                  Un-inline general styles and deploy to SOYMAIL.CSS
                  config [public-footer] allows attribution ("Thanks to ...")
                  config [site-style-sheet] <url> (loads after theme)
                  refine HTML to bring it more in line with modern standards
18-APR-2013  MGD  v1.6.3,
                  InetMailParseHeader() allow for a bloody-minded and probably
                    illegal combination of <LF> and <CR><LF> 
22-JUL-2012  MGD  v1.6.2,
                  MtaMsgToFile() accomodate addresses containing hyphens
                  InetMailParseHeader() liberalise RFC822 detection
                  bugfix; InetMailParseHeader() ignore <cr>s at end-of-header
                  bugfix; RequestPrivate() reset state if username changes
                  bugfix; DraftSave() force references multiple lines (per WK)
27-MAR-2011  MGD  v1.6.1
                  NewMailScriptAjax() refresh if message count decreases
                  bugfix; CallMailGetuserProfile() CGIplus new mail detection
                  bugfix; AttachComposeList() uninitialised pointer
                    if attachment file did not produce a content-type
                  bugfix; AddressListMassage() comma processing
03-APR-2010  MGD  v1.6.0
                  WASD CGIplus reduces response latency to 50%-20% of CGI!
                    redirect /cgi-bin/soymail/~ /cgiplus-bin/soymail/~
                  CgiLibVeeMem..() to accomodate CGIplus persistence
                  MessagePreView() and associated JavaScript
                  add [^] to [contacts] button
                  config [context-cache-expires]
                  option [preview-after]
                  option [preview-size] (manual)
                  option [you-got-mail-at]
                  NewMailAt()
                  HtmlToPlain() supply charset for UTF-8 deentify
                  HtmlCharacterDeEntify() allow for UTF-8 charset
                  ComposePage() UTF-8 encode signature when required
                  add a DTD DOCTYPE for HTML 4
                  add style="border:none;" to all checkboxes to work-around
                    annoying MSIE (of course) ugly border
09-AUG-2009  MGD  v1.5.3,
                  LoginAcmeVerifyUser() only employ ACME$_TARGET_DOI_NAME
                    when configured otherwise ACME$LATEST_ENABLED_AGENT_LIST
                  authentication for public access
                  config [public-no-access]
                  ConfigPublicAccess() [] authorized public token
                  LoginAgentVerifyUser() SS$_CONTROLO (1545) indicates
                    that the credentials are OK but the user has no access
                    to the area (introduced for authorized public access)
                  bugfix; InetMailParseHeader() end-of-header detection
11-MAR-2009  MGD  v1.5.2,
                  InetMailParseField() try to second-guess when a VMS record
                    buffer overflowed due to unbreakable character sequence
                  InsertMessage() utility and /INSERT=<parameters>
                  InetMailGetField() improve parsing of leading continuation
                  ComposePageRequest() improve generation of references header
                  bugfix; InetMailFoldRfc822Header() line folding
29-NOV-2008  MGD  v1.5.1,
                  when VMS-newmsg set comply with VMS Mail handling
                    of the new mail count, do not reset new message flag
                  config [VMS-newmsg]
                  option [VMS-newmsg]
                  CgiLibHtmlEscape() retain empty URLs as text
                  support for RFC "In-Reply-To:" and "References:"
                  ProcessRequest() new mail check NewMailLoginFail()
                    (a better solution to the inadvertant MFI DOS)
                  bugfix; InetMailParseHeader() empty field from 23-SEP-2008
26-OCT-2008  MGD  v1.5.0,
                  ProcessRequest() new mail check sleep (inadvertant MFI DOS)
                  InetMailParseHeader() if a field is empty just ignore it
                    (prompted by an empty reply-to)
                  MimeDecBase64() relax (ignore) accepted characters
                    outside of encoding sequence
                  MimeDecParseBody() report premature end-of-message
                  InetMailParseHeader() manufacture MIME for message with
                    content-type of HTML but no MIME version header (kosher?)
                  MessageHeader() [^] immediate edit when adding contact
                  MessagePageMsgRequest() change contact addition processing
                  modify options page layout (thanks Bill Korendyk)
                  ComposeSigFileLoad() allows [SMTP-from] and [SMTP-reply-to]
                    directives to specify the "From:" and  "Reply-to:" message
                    header fields (as well as adding any signature of course)
                  support for non-Latin character set (e.g. Arabic) messaging
                    having novel (to me) script directionality requirements
                  user and global compose character sets (see COMPOSE.C)
                  major revision of PMDF IMAP processing
                  ComposePage() JavaScript support for searching contacts list
                  ReadFileIntoMemory() optimise for stream format files
                  generation of LDIF contact lists for the compose page
                    has been significantly revised resulting in a 6800 entry
                    load time dropping from 75 seconds to under 2 seconds!
                  ContactsLdifPurge() command-line utility (/LDIF=PURGE)
                  ContactsLoadFile() differentiate LDIF and VMS mailing
                    list files using characteristics unique to LDIF
                  [compose][^] (new window) from compose page always results
                    in a fresh compose page ([compose] 'regenerates' the page)
                  ComposePageRequest() ?mailto: query string (from below)
                  MessageBodyText() and MessagePrintView() hack of CGILIB
                    to make "mailto:"s access soyMAIL as mail agent
                  allow change of (expired) password (via login dialogs)
                  NewMailScriptRefresh() add 'beeps' for
                     browsers/environments not supporting WAVs
                  AttachComposeList() accomodate [attachment-MIME-types]
                  "From:" composition field (enabled by [compose-user-from])
                  option [compose-charsets]    per-user override for config
                  option [compose-edit-cols]   per-user overrides for default
                  option [compose-edit-rows]   and/or soyMAIL configuration
                  option [compose-wrap-at]     directives
                  option [options-panel-expand]  Wm(B)K manual edit
                  option [PMDF-IMAP] enables PMDF IMAP read compatibility
                  option [smtp-from] can be specified as an exclamation mark
                     to allow config *user options override* to disable
                  config [attachment-MIME-types] (see example in MESSAGE.C)
                  config [compose-charsets]  list of character sets to be
                                             presented on composition page
                  config [compose-contacts-after] expand contacts list
                  config [compose-contacts-size] expand to this number of "em"
                  config [compose-edit-cols] e.g. "72,80,96,132"
                  config [compose-edit-rows]   or "72 80 96 132"
                  config [compose-user-from] enables composition page "from:"
                  config [compose-wrap-at]   "72,80,96,132"
                  config [include-file] directive
                  config [login-alias] directive
                    (and associated functionality in LOGIN.C)
                  config [login-alias-SMTP-from] directive
                  config [login-type] directive
                    (and associated functionality in LOGIN.C)
                  config [user-name-info] display username in status info
                  bugfix; JavaScript confirmSend() use htmlEditorContent()
                    to determine whether an HTML editor has content or not
                    (NOTE that the initialisation JavaScript has changed!)
11-MAY-2008  MGD  v1.4.2,
                  MtaMsgSmtpToServer() allow but do not RCPT any address 
                    "(whatever)"@whatever (with BCC allows non-disclosure)
                  CgiLibHtmlDeEntify() enhance entity scope
                  modify all .CSS so that 'font-family: fixed, monospace;'
                  is 'font-family: monospace, fixed;' (for KDE FireFox)
                  bugfix; CallMailFileIn() do not prefix if name contains
                    a device or logical name (i.e. if ':' present)
                  bugfix; CallMailFileIn() if SYS$LOGIN: present in a file
                    name get the username home directory and use that!
                  bugfix; CallMailFileIn() do not munge if ODS-5 volume
                  bugfix; RequestPrivate() and RequestParsePath()
                    allow for GET request URI without /~
                  bugfix; LoginPage() remove HTML-escaping of login message
30-NOV-2007  MGD  v1.4.1,
                  CallMailFileClose() accomodate bogus OPENIN return value
                    (see description of issue in function prologue)
13-NOV-2007  MGD  v1.4.0,
                  support multiple signature files
                  LoginAgentVerifyUser() and [login-agent] external
                    agent authenticator (see description in LOGIN.C)
                  LoginGetUaiUser() behave more like LoginAcmeVerifyUser()
                    authenticator by only checking password expiry flags
                  allow 'for non-script' access URLs to soyMAIL
                    (see example of dedicated soyMAIL Web service in CONFIG.C)
                  LangOctalEntity() to octal-escape HTML entities for
                    use in JavaScript alert() and confirm() strings 
                  ComposeSendClose() returns to the message listing when
                    it's not a child window ([send-close] is always active)
                  RequestPrivate() and RequestPublic() when
                    [user-options-override] then OptionsParse()
                  any options specified using [user-options-override]
                    disables the corresponding option in the user setup page
                  add [message-flags] configuration directive
                    (this is currently an non-internationalised feature)
                  CallMailMessageGet() resets the NEWMSG flag as appropriate
                  MessageFolderList() add JavaScript to popup a message by
                    using any of the 'Received', 'From' or 'Subject' fields
                  add [login-acme-no-restrict] and LoginAcmeVerifyUser()
                    ignore LGI$_RESTRICT if authentication OK
                  relax 'cannot copy to' sent items (allows NEWMAIL, etc.)
                  DraftSave() and DraftRestore() [charset] to propagate
                    any compose message character set
                  removed (somewhat lame) read history facility and
                    greyed-out the user-option in the interim
                  SoyMailJavaScriptStuff() finally discovered and kludged
                    around the Opera child window [close] button issue
                  MtaMsgSmtpToServer() improve error detection and reporting
                  bugfix; AddressListMassage() [smtp-default-host]
                    detect VMS Mail override using explicit 0::<username>
                  bugfix; ComposeMime() obtain and propagate request ID and
                    hash from state data if unavailable from request data
                  bugfix; delete empty mail files after msg move (and deletion)
                  bugfix; ComposePage() charset
                  bugfix; MessageBodyProcess() return if RFC822 header error
                  bugfix; AddressIsVms() and AddressIsRfc() requires
                    AddressListExtract() to parse an address from a list
13-MAY-2007  MGD  v1.3.3,
                  [access-log] directive and RequestAccessLog() to provide
                    combined format log file for autogenous authentication
                  CGILIB emulate CGI variable REQUEST_URI for OSU
                  CallMailCopyFile() update newmail count if necessary
                  OptionsWithinReason() ensure draft and waste items
                    folder names cannot be set to MAIL or NEWMAIL
                  refine (fiddle with) structure member alignment
                    (after discovering NETDB.H in MTA.C broke it!)
                  ComposeMime() refine (or at least fiddle with) the
                    originating "Received:" header format
                  MessageBodyProcess() refine RFC822 header error reporting
                  SearchMessage() refine RFC822 header error processing
                  bugfix; MessagePageFolderRequest() apply 'MessageCountDelta'
                    to 'RangeIdStart' after MOVE, WASTE, DELETE or EMPTY 
                  bugfix; CallMailFileList() set name length from PMDF folder
26-MAR-2007  MGD  v1.3.2,
                  bugfix; autogenous login with OSU
                  bugfix; CgiLibOsuInit() <DNETHDR> no URL-decode
24-MAR-2007  MGD  v1.3.1,
                  AttachComposeList() JavaScript-enable the [attach] button
                  AttachUpload() massage MSIE filenames with full path
                    (fixes checkbox "checked"ing when MSIE upload attachments)
                  bugfix; RequestVmsMailContext() establishes file/folder 
                    context (fixes issue with folder button functionality)
16-MAR-2007  MGD  v1.3.0,
                  soyMAIL autogenous authentication and logout
                  multiple Mail file [copy][move] destination selectors
                     provide all files (drawers) and associated folders 
                  MessageBodyButtons() rearrange [delete][waste] buttons
                    to be that same order as MessagePageFolderRequest()
                  CallMailFileClose() enforce full-close purge of WASTEBASKET
                  CallMailDestFolderList() list all folders in all mail files
                  CallMailMessageDelete() now checks non-MAIL.MAI mail files
                    and deletes them if empty (contain no folders)
                  AttachComposeList() checkbox to 'delete all attachments'
                    after a successful message send 
                  ComposeSendMessage() if 'delete all attachments' checkbox 
                    delete all attachments after successful send
                  SearchGetString() modify to allow significant spaces
                  NewMailScriptRefresh() and NewMailScriptAjax() cater
                    for MSIE initial IFRAME load to avoid disruptive
                    initial 'check new mail' message in status panel
                  specially process PMDF IMAP folder subdirectories
                    see CallMailPmdfImapFolder() for explanation
                  [message-attachment-panel] is a manual option that positions
                    attachment panel above or below (default) the message body
                  [draft-items] user option for specify name of drafts folder
                  [sent-items] user option for automatically saving sent items
                  [waste-items] user option for specifying [waste] folder
                  Purveyor has been explicitly excluded because of issues
                    with it's CGI <stdout> IPC mailbox overflowing at 1024
                    characters and this generating spurious newlines with
                    "text/.." response MIME types
                  bugfix; MessageAttachments() perform URL_ENCODE() rather
                    than HTML_ESCAPE() on buttons for message attachment URLs
                  bugfix; MimeDecParseBody() add MimeDecProcessPart() to
                    message/rfc822 processing to cope with content encoding
                  bugfix; InetMailParseField() handling continuation
                   line after <CR><LF> carriage-control ("Snt Items")
30-JUL-2006  MGD  v1.2.2,
                  bugfix; CerealDataOut() perhaps those ambits were not the
                    the issue, set a minimum buffer space of 256 (to catch
                    possible zero-length data streams that still need a hash)
22-JUL-2006  MGD  v1.2.1,
                  rework reply-all address compilation
                  try to make font width allowances for monospace
                  add [message-order] user option directive
                  AddressListMassage() allow for comma-separated addresses
                  MessageFolderList() user-optional message ordering
                  PublicMessageFolderList() user-optional message ordering
                  bugfix; ContactsLdifRecord() HTML-escape only non-base64
                  bugfix; ComposePage() HTML-escape the hidden signature
                  bugfix; ComposeTextMassage() retain quote-char as a string
                  bugfix; ComposeSendMessage() check reply-to for VMS address
                    (Non- info-WASD subscribers are lucky to have info-WASD
                    subscribers get all of my stupid errors winnowed-out
                    before public release.)
12-JUL-2006  MGD  v1.2.0,
                  ComposeCopySelf() to refine interactions between VMS mail
                    addresses, [smtp-default-host], and copy-self, etc.
                  add [forward]ing (from message read page) of attachments
                    with ComposeReplyAttachList(), ComposeForwardAttachList()
                    and changes to ComposeMime() to add the forwarded parts
                  AttachFileExtract() to extract message parts as
                    user-accessable files into the user home directory
                  CallMailUserDiskQuota() and MessageDiskQuotaUsed() notifies
                    the client with each folder open once disk quota reaches
                    the percentage specified by default (85%) ro configuration
                  HelpAbout() adds a disk quota report item
                  ComposeRfcDate() to derive RFC822 "Date:" from client via
                    JavaScript to better represent message origination time
                  maximum count on JavaScript 'working' status (periods)
                  identify and make available any HTML content in a non-MIME
                    message or in MIME text/plain in-line content
                  add [disk-quota-percent] and [vms-occlude] config directives
                  add [sig-file] user option directive
                  remove the [check-new-mail-nonajax] and [check-new-mail-MSIE]
                    user option directives and associated kludges
                  SearchPage()  results every "all hit(s)"
                  bugfix; AddressListMassage() better VMS address detection
                  bugfix; CallMailMessageList() restore from temporary
                    'MessageContext' when searching
                  bugfix; HtmlSkipTagTo() quotes outside of tags
                  bugfix; contact import and merge not broken but brain-dead
                    revision of ContactsImportLdif() and ContactsMergeLdif()
                  bugfix; ContactsLoadFile() do not munge non-personal (i.e.
                    site-specific) contact file name
06-MAY-2006  MGD  v1.1.1,
                  MessageHeader() show reply-to: field when different to from:
                  ComposePageRequest() reply-to/all to reply-to: if available
                    otherwise to from: 
                  MtaMsgSmtpToServer() allow a port number to be specified
                    with the SMTP server host (i.e. "the.host.name:25")
                  optimise CallMailMessageHeader() by completely replacing
                    mail$message_get() with mail$message_info() and adding
                    mail$message_get() to CallMailMessageGet() for non-external
                    mail messages only (suggested by Dave Jones)
                  accessability option [accessability-1]
                  bugfix; AddressIsRfc() RFC2822 only allows the double
                    quotation character (0x34) to quote
                  bugfix; AddressIsRfc() and MtaAddressExtract() allow
                    backslash (0x92) to escape a single character 
                  bugfix; PublicMessageRequest() detect [search] button
                  bugfix; ContactsLoadFile() rdptr->ContactsTextPtr = NULL
                    so that empty LDIF file is not confused with VMS list
                  bugfix; CallMailFileList() parsing of directory entry
                    name length and entry flags (per Dave Jones) and
                    use memmove() for shuffling inside the one buffer
18-APR-2006  MGD  v1.1.0, 
                  add configuration directive [private-request] and modify
                    behaviour to determine private or public access
                  add configuration directive [update-last-login] with
                    RequestLastLogin() optionally sets private access
                    SYSUAF last-login at first (initial GET) access 
                  bugfix; ComposeSelfAddress() size for calloc()
                  bugfix; RequestPublic() parsing folder with GET
09-APR-2006  MGD  v1.0.0,
                  add [send-close] and ComposeSendClose()
                  significant rework to rationalise public and private
                  remove default MAIL and NEWMAIL folders in listings
                  MessagePage() and PublicMessagePage() precede each
                    message part/attachment with an anchor/link to allow
                    for right-click browser save-as 
                  LangLoad() in conjunction with LANGDEF.H used to provide
                    details on missing/extra entries in a language file
                  ConfigPublicAccess() support wildcard folder mapping
                    in conjunction with extensive changes to PUBLIC.C
                  bugfix; OptionsSoyMail() propagation of personal
                    you-got-mail when when changing language option
                  bugfix; CerealDataOut() allow ambit 3x buffer space
27-MAR-2006  MGD  v0.4.3,
                  [wrap-text-at] transmogrified to [massage-read]
                    option now wraps long lines, eliminates extra line breaks
                    and removes any PGP signature
                  MessagePgpSigned() and MessagePgpCut() for PGP signature
                  small accomodation for Purveyor around 'binary' fwrite()s
                  RequestPrivate() Purveyor (1.2 at least) requires a full
                    URL to redirect (during logout)
                  refine 'watch' detail including MTA SMTP identification
                  'checking for new mail' uses beginWorking() progress dots
19-MAR-2006  MGD  v0.4.2,
                  AttachFile() ensure any file system path is stripped
                    from the attachment name before saving (MSIE)
                  refine HtmlNumericDeEntify() and HtmlCharacterDeEntify()
                  LangSame() modify comparison so that the message can
                    contain HTML entities useful in some buttons (e.g. &#10;)
                  AddressListMassage() refine address reduction (yep - again!)
                    also allow for appending a default host/domain if the
                    configuration directive [SMTP-defaulthost] is set
                  configuration directive [logout-realm] added to allow logout
                     for Apache (must be set the same as 'AuthName')
                  MimeDec8bit() disable reporting and always return OK
                  refine contact management wrt duplicate common names
                  bugfix; InetMailParseHeader() non-MIME MIME-like headers
                  bugfix; ContactsSave() to empty LDIF file
                  bugfix; MessageBodyIdentify() when generating plain from
                    HTML-only content call MimeDecProcessPart() to decode
                  bugfix; MessageBodyIdentify() default a non-MIME message
                    message to 8-bit, plain-text
13-MAR-2006  MGD  v0.4.1,
                  HtmlSanitise() suppress "background-image"
                  bugfix; ComposeSelfAddress() 'len' initialization
12-MAR-2006  MGD  v0.4.0,
                  MimeDecParseBody() return FALSE on detectable parse error
                    while all callers of this function check return and 
                    process on success or failure of the parse
                  MimeDecParseError() to report MIME parse problems
                  move input focus to the field most appropriate to the stage
                    of message composition, defaulting to the message text
                  remove 'noisy' message count from [message] range button
                  RequestPrivate() no longer include the username when
                    generating the form action URI unless POSTMASTER
                    facility in use (this reduces the leakage of potentially
                    sensitive information through the "Referer:.." header)
                  bugfix; RequestPrivate() do not upper-case username when
                    parsing path as it can interfere with VMS external auth
                  bugfix; ComposeSelfAddress() with rationalization of 'self' 
                    address generation and using this for the "From:" header
                  bugfix; allow MIME related headers (e.g. "Content-type:")
                    to precede the "MIME-version:" header (but cancel MIME
                    if it is not ultimately found in the message header)
04-MAR-2006  MGD  v0.3.9,
                  MessagePage() add MessageButtons() after longer messages
                  bugfix; CgiLibEnvironmentInit() for VMS Apache V2.n
                    and later (as with Apache V1.n) freopen("mrs=16384")
27-FEB-2006  MGD  v0.3.8,
                  ComposePageRequest() support VMS copy-send/forward/reply
                  CallMailGoToNewMail() if the new message count differs
                    from the messages selected count then adjust it
                  ComposeSendMessage() expand MX_.. logical names that
                    can be used to generate the "From:" address
                  ComposePage() [signature] button only if the user has a
                    signature file specified
                  RequestPrivate() refine [logout] mechanism to improve
                    overall behaviour and avoid hole if MSIE [cancel]s
                  [logout] now has associated JavaScript confirm explaining 
                    the logout basics and allowing an opportunity to cancel
                  [close] now has a JavaScript confirm for compose page
                  configuration directive [greeting] added to allow an
                    initial site-specific message shown in the status panel
                  OptionsWithinReason() make default OPTIONS_READ_PLAIN1_HTML2
                  refine MessageBodyIdentify() to manufacture plain-text
                    content when there is only HTML content
                  bugfix; MainMenuRequest() FOLDER_LIST_CHANGE detect 't'
                  bugfix; CallMailMessageDelete() close mail file to
                    empty WASTEBASKET before continuing request
                  bugfix; CallMailMessageCopyMove() adjust new message
                    count regardless of whether it's a copy or a move
16-FEB-2006  MGD  v0.3.7,
                  when used from the message page the main menu [compose]
                    button should provide a blank message, when used from the
                    compose page it should propagate any in-composition message
                  main menu [close] button for child windows
                  AddressListMassage() refine "<address>, <address>" reduction
                  provide more informative message if CallMailGetUserProfile()
                    reports MAIL$_NOSUCHUSER because of no profile entry
                  bugfix; MimeDecProcessPart() sanity check on pointer
                    not length (after all you can have empty content)
                  bugfix; ContactsEdit() no sanity check on un-specified
                    contact, just memset() zero it and continue as 'new' entry
12-FEB-2006  MGD  v0.3.6
                  optional folder buttons on left-side of message listing
                  support saving message as "message/rfc822" 'attachment'
                  refine MimeDecParseBody() to accomodate "message/rfc822"
                    attachments and any such internal MIME parts
                  modified [reply-to] option to [smtp-from] and in conjunction
                    with [personal-name] have ComposePageRequest() generate a
                    "From:" header more in line with TCPIP$SMTP_FROM behaviour
                  add explicit [reply-to] button and field so that messages
                    can individually have a reply-to set against them
                  ComposeMime() add client traceability information to
                    "Received:" header field
                  bugfix; default charset should be ISO-8859-1 not IS0-8859-1
                  bugfix; AttachComposeList() attachment size calculation
                  bugfix; RequestParsePath() attachment names can contain '/'
                  bugfix; SearchKeyword() lower-case the keyword!
27-JAN-2006  MGD  v0.3.5
                  initial public BETA release
01-FEB-2005  MGD  development commences
*/
/*****************************************************************************/

#define COPYRIGHT_DATE "2005-2025"
#define SOFTWAREVN "2.2.0"
/* don't forget to ^^^^^^^ update SOYMAIL.JS compatibility array! */
#define SOFTWAREVN_STATE_FIDDLE_FACTOR "\0"
#define SOFTWARENM "SOYMAIL"
#ifdef __ALPHA
#  define SOFTWAREID SOFTWARENM " AXP-" SOFTWAREVN
#endif
#ifdef __ia64
#  define SOFTWAREID SOFTWARENM " IA64-" SOFTWAREVN
#endif
#ifdef __VAX
#   error VAX no longer implemented
#endif
#ifdef __x86_64
#  define SOFTWAREID SOFTWARENM " X86-" SOFTWAREVN
#endif
#define SOFTWARECR "Copyright (C) 2005-2025 Mark G.Daniel"

#define SOFTWARE_APACHE_2 \
"Licensed under the Apache License, Version 2.0 (the \"License\");\n\
you may not use this software except in compliance with the License.\n\
A copy is available at ... http://www.apache.org/licenses/LICENSE-2.0\n\
Software distributed under the License is distributed on an \"AS IS\" BASIS,\n\
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n"

#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 <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unixlib.h>

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

/* application specific */
#include "soymail.h"
#include "cereal.h"
#include "callmail.h"
#include "cgilib.h"
#include "config.h"
#include "login.h"
#include "mainmenu.h"
#include "mta.h"
#include "newmail.h"
#include "other.h"
#include "request.h"
#include "message.h"

#define CLI$_INSFPRM 229448
#define CLI$_OVRFLW 229728

#define FI_LI "SOYMAIL", __LINE__

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

BOOL  Debug,
      CliLdifPurge,
      ContextCacheDefer,
      ContextCacheInUse,
      IsCgiPlus,
      WatchEnabled;

int  CgiPlusUsageCount,
     ContextCacheCount,
     ContextCacheCurrent,
     ContextCacheIndex,
     ContextCacheNext,
     ExitStatus,
     SoyMailVolumeStruct,
     VmsVersion;

unsigned int  CurrentTimeSeconds;

int64  CurrentTime64;
unsigned short  CurrentNumTime [7];

char  *CgiEnvironmentPtr,
      *CgiRequestUriPtr,
      *CliParam1Ptr,
      *CliParam2Ptr,
      *SoyMailScriptPath = "/soymail/-/";

char  CopyrightDate [] = COPYRIGHT_DATE,
      DocType [] = SOYMAIL_DOCTYPE,
      SoftwareCopy [] = SOFTWARECR,
      SoftwareApache2 [] = SOFTWARE_APACHE_2,
      SoftwareId [] = SOFTWAREID,
      /* with fiddle-factor to allow data structure changes inside a version! */
      SoftwareVn [16] = SOFTWAREVN SOFTWAREVN_STATE_FIDDLE_FACTOR,
      Utility [] = "SOYMAIL";

char  CommandLine [256],
      CurrentVmsTimeString [48],
      LocalHostName [128],
      SoftwareEnv [96],
      SoyMailQueryVersion [] = "?" SOFTWAREVN;

/* callable mail thread limit */
#define CONTEXT_CACHE_MAX 31
static struct STRUCT_CONTEXT_CACHE
{
   unsigned int  ExpiresSeconds;
   unsigned long  VmsMailFileContext,
                  VmsMailUserContext;
   char  UserName [USERNAME_MAX+1];
} ContextCache [CONTEXT_CACHE_MAX];

EXIT_HANDLER  ExitHandler;
USER_OPTIONS  UserOptions;
REQUEST_DATA  RequestData;
VMS_MAIL_USER  VmsMailUser;

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

extern  CONFIG_DATA SoyMailConfig;

/**************/
/* prototypes */
/**************/

void ChangeProcessName ();
void soyMAILexit ();

int sys$dclexh (__unknown_params);
int sys$gettim (__unknown_params);
int sys$getjpiw (__unknown_params);
int sys$numtim (__unknown_params);
int sys$setprn (__unknown_params);
int sys$setimr (__unknown_params);

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

main (int argc, char* argv[])
       
{
   int  status;
   int  ProctoredScript = -1;
   VMS_MAIL_MSG  MailMsg;
   VMS_MAIL_USER  UserData;

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

   CgiLibVeeMemInUse (1);

   sprintf (SoftwareEnv, "%s, %s, %s",
            SOFTWAREID, (char*)CgiLibEnvironmentVersion(), BUILD_DATETIME);

   if (getenv ("SOYMAIL$DBUG"))
   {
      /* some contortions to accomodate OSU */
      Debug = TRUE;
      if (!getenv ("WWWEXEC_RUNDOWN_STRING"))
      {
         fprintf (stdout,
"Content-Type: text/plain\nScript-Control: X-content-encoding-gzip=0\n\n");
         CgiLibEnvironmentSetDebug (1);
      }
   }

   CgiLibEnvironmentInit (argc, argv, TRUE);
   if (Debug) CgiLibEnvironmentRecordOut ();

   if (CgiLibEnvironmentIsPurveyor())
   {
      fprintf (stdout, "%%%s-E-PURVEYOR, not supported!\n", Utility);
      exit (STS$K_ERROR | STS$M_INHIB_MSG);
   }

   if (Debug)
   {
      /* continuing contortions */
      if (getenv ("WWWEXEC_RUNDOWN_STRING"))
      {
         fprintf (stdout, "Content-Type: text/plain\n\n");
         CgiLibEnvironmentSetDebug (1);
      }
   }

   GetParameters (argc, argv);

   if (CliLdifPurge) exit (ContactsLdifPurge (CliParam1Ptr, CliParam2Ptr));

   CgiLibResponseSetSoftwareID (SoftwareId);
   CgiLibResponseSetErrorStatus (403);
   CgiLibResponseSetErrorMessage ("reported by soyMAIL");

   CgiEnvironmentPtr = CgiLibEnvironmentName ();
   VmsVersion = GetVmsVersion ();

   CgiLibUrlMaxShorten (64);

   IsCgiPlus = CgiLibEnvironmentIsCgiPlus ();

   ChangeProcessName ();

   /* set up and declare the exit handler */
   ExitHandler.HandlerAddress = (unsigned long)&soyMAILexit;
   ExitHandler.ArgCount = 1;
   ExitHandler.ExitStatusPtr = (unsigned long)&ExitStatus;
   status = sys$dclexh (&ExitHandler);
   if (VMSnok (status)) exit (status);

   if (IsCgiPlus)
   {
      /* these are harmless should context caching be disabled */
      ContextCacheExpire ();

      for (;;)
      {
         /* block waiting for the next request */
         CgiLibVar ("");
         CgiPlusUsageCount++;

         if (!Debug && getenv ("SOYMAIL$DBUG"))
         {
            fprintf (stdout, "Content-Type: text/plain\n\n");
            CgiLibEnvironmentSetDebug (Debug = 1);
         }

         if (ProctoredScript < 0)
         {
            /* check for proctor instantiation */
            ProctoredScript = !*CgiLibVar ("REMOTE_ADDR") &&
                              !*CgiLibVar ("SERVER_ADDR");
            if (ProctoredScript)
               fprintf (stdout, "Status: 204\n\n");
            else
               ProcessRequest ();
         }
         else
            ProcessRequest ();

         CgiLibEnvironmentSetDebug (Debug = 0);
         CgiLibCgiPlusEOF ();
         CgiLibVeeMemInit ();
      }
   }
   else
      ProcessRequest ();
}

/****************************************************************************/
/*
Exit handler.
*/ 

void soyMAILexit (int *ExitStatusPtr)

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

   if (TrnLnm ("SOYMAIL_OPCOM_EXIT", NULL, 0))
   {
      /* development purposes only */
      char  *sptr = StackTrace (FI_LI);
      OpcomMessage ("soyMAIL exit %X!8XL\r\n!AZ", *ExitStatusPtr, sptr);
   }

   if (IsCgiPlus) ContextCacheClose ();

   ChangeProcessName ();
}

/****************************************************************************/
/*
First call; save the current process name and set the soyMAIL process name.
Second call; reset to original process name (just in case).
*/ 

void ChangeProcessName ()

{
   static unsigned long  JpiPidItem = JPI$_PID;
   static unsigned long  JpiPrcNamItem = JPI$_PRCNAM;
   static char  PrcNam1 [16],
                PrcNam2 [16];
   static $DESCRIPTOR (PrcNam1Dsc, PrcNam1);
   static $DESCRIPTOR (PrcNam2Dsc, PrcNam2);

   int  status;
   unsigned long  ScriptPid;
   struct dsc$descriptor_s  *dscptr;

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

   if (!PrcNam1[0])
   {
      lib$getjpi (&JpiPrcNamItem, 0, 0, 0, dscptr = &PrcNam1Dsc, 0);
      dscptr->dsc$a_pointer[dscptr->dsc$w_length] = '\0';
      /* if v12.0 or later leave name alone */
      if (PrcNam1[0] == '/') return;
      lib$getjpi (&JpiPidItem, 0, 0, &ScriptPid, 0, 0);
      dscptr = &PrcNam2Dsc;
      dscptr->dsc$w_length = sprintf (PrcNam2, "soyMAIL_%4.4X",
                                      ScriptPid & 0xffff);
   }
   else
   {
      if (PrcNam1[0] == '/') return;
      dscptr = &PrcNam1Dsc;
   }

   if (VMSnok(status = sys$setprn (dscptr))) ErrorExit (status, FI_LI);
}

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

ProcessRequest ()

{
   int gethostname (char*, int);

   BOOL  UseCache;
   int  idx, status,
        PostBufferCount;
   char  *cptr, *sptr, *zptr,
         *CodicilPtr = NULL,
         *PostBufferPtr;
   REQUEST_DATA  *rdptr;

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

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

   /* (CGIplus) initialise */
   StatTimer (FALSE);
   CgiLibResponseSetCharset (NULL);
   StatusMessage (NULL, 0, 0, NULL);

   sys$gettim (&CurrentTime64);
   sys$numtim (&CurrentNumTime, &CurrentTime64);
   PrintFao (CurrentVmsTimeString, sizeof(CurrentVmsTimeString),
             "!20%D", &CurrentTime64);
   CurrentTimeSeconds = decc$fix_time (&CurrentTime64);

   if (cptr = TrnLnm ("SOYMAIL$WATCH", NULL, 0))
   {
      if (Debug) fprintf (stdout, "SOYMAIL$WATCH |%s|\n", cptr);
      WatchEnabled = TRUE;
      if (isdigit(*cptr))
      {
         /* if it doesn't match the remote address then disable again */
         if (!(sptr = CgiVar ("REMOTE_ADDR"))) sptr = "";
         if (strcmp (cptr, sptr)) WatchEnabled = 0;
      }
   }

   if (Debug) WatchEnabled = TRUE;

   if (WatchEnabled)
   {
      /* initialize watch mode */
      CgiLibEnvironmentRecordOut();
      WatchThis (FI_LI, NULL);
      WatchThis (FI_LI, "SYSTEM !AZ", GetPlatformDescription());
      cptr = CgiLibVarNull ("SERVER_SOFTWARE");
      if (!cptr) cptr = "?";
      WatchThis (FI_LI, "SERVER !AZ", cptr);
      WatchThis (FI_LI, "!AZ", SoftwareId);
      cptr = CgiLibVarNull ("HTTP_USER_AGENT");
      if (!cptr) cptr = "?";
      WatchThis (FI_LI, "AGENT !AZ", cptr);
   }

   memset (&UserOptions, 0, sizeof(UserOptions));
   memset (&RequestData, 0, sizeof(RequestData));
   memset (&VmsMailUser, 0, sizeof(VmsMailUser));

   rdptr = &RequestData;

   /* reset this to empty, it'll be set "Set-Cookie:.." if needed */
   rdptr->LoginSetCookiePtr = "";

   /* some of these will be used during configuration so get 'em now */
   CGIVAR (rdptr->CgiRequestMethodPtr, "REQUEST_METHOD");
   CGIVAR (rdptr->CgiRequestUriPtr, "REQUEST_URI");
   CGIVAR (rdptr->CgiScriptNamePtr, "SCRIPT_NAME");
   CGIVAR (rdptr->CgiPathInfoPtr, "PATH_INFO");
   CGIVAR (rdptr->CgiQueryStringPtr, "QUERY_STRING");

   CGIVAR (rdptr->CgiHttpUserAgentPtr, "HTTP_USER_AGENT");
   if (strstr (rdptr->CgiHttpUserAgentPtr, "Chrome"))
      rdptr->UserAgent = SOY_USER_AGENT_CHROME;
   else
   if (strstr (rdptr->CgiHttpUserAgentPtr, "Firefox"))
      rdptr->UserAgent = SOY_USER_AGENT_FIREFOX;
   else
   if (cptr = strstr (rdptr->CgiHttpUserAgentPtr, "MSIE"))
   {
      /* this indicates it's Explorer */
      rdptr->UserAgent = SOY_USER_AGENT_MSIE;
      cptr += 4;
      while (*cptr && *cptr == ' ') cptr++;
      /* this indicates it's Explorer 10 or later */
      if (atoi(cptr) >= 10)
         rdptr->UserAgent = SOY_USER_AGENT_MSIE10;
      CgiLibXUACompatible ("IE=edge");
   }
   else
   if (strstr (rdptr->CgiHttpUserAgentPtr, "Opera"))
      rdptr->UserAgent = SOY_USER_AGENT_OPERA;
   else
   if (strstr (rdptr->CgiHttpUserAgentPtr, "Safari"))
      rdptr->UserAgent = SOY_USER_AGENT_SAFARI;
   else
   /* Mozilla occurs in some of the above agent strings and so comes last */
   if (strstr (rdptr->CgiHttpUserAgentPtr, "Mozilla"))
      rdptr->UserAgent = SOY_USER_AGENT_MOZILLA;
   else
      rdptr->UserAgent = SOY_USER_AGENT_UNSPECIFIED;

   /* load the soyMAIL configuration */
   ConfigLoad (rdptr);

   if (SoyMailConfig.DowntimeMessage)
   {
      CgiLibResponseHeader (503, "text/html");
      fprintf (stdout, "%s &nbsp;&nbsp;&mdash;&nbsp;&nbsp %s\n",
               InetMailRfcVmsDate(CurrentVmsTimeString),
               SoyMailConfig.DowntimeMessage);
      return;
   }

   /* SMTP server identification (must be done after the configuration load) */
   if (WatchEnabled) MtaSmtpServerPoll (SoyMailConfig.SmtpServerHost);

   if (rdptr->CgiQueryStringPtr[0])
   {
      cptr = rdptr->CgiQueryStringPtr;
      if (!strcmp (cptr, "help") || !strncmp (cptr, "help+", 5))
      {
         /* public access to the consolidated help page */
         OptionsPublicGet (rdptr);

         cptr += 4;
         if (*cptr == '+')
         {
            cptr++;
            zptr = (sptr = rdptr->UserOptions.Language) +
                   sizeof(rdptr->UserOptions.Language)-1;
            while (*cptr && sptr < zptr) *sptr++ = *cptr++;
            *sptr = '\0';
         }

         LangLoad (rdptr->UserOptions.Language);
         if (!rdptr->UserOptions.CharSet[0])
            strcpy (rdptr->UserOptions.CharSet, LangFor("lang_charset"));

         HelpAll (rdptr);
         return; 
      }
   }

   if (!strcmp (rdptr->CgiRequestMethodPtr, "POST"))
   {
      /********/
      /* POST */
      /********/

      /* read and create the CGI variable equivalents of the body fields */
      CgiLibReadRequestBody (&PostBufferPtr, &PostBufferCount);
      if (WatchEnabled) WatchThis (FI_LI, "POST !UL bytes", PostBufferCount);
      if (!PostBufferPtr || !PostBufferCount) ErrorExit (SS$_BUGCHECK, FI_LI);
      if (WatchEnabled)
         WatchThis (FI_LI, "BODY !UL bytes\n!AZ", PostBufferCount, PostBufferPtr);
      CgiLibFormRequestBody (PostBufferPtr, PostBufferCount);
      if (WatchEnabled)
         while ((cptr = CgiLibVarNull ("*")) != NULL)
            WatchThis (FI_LI, "!AZ", cptr);
      /* now we can start to use the FORM_.. variables */
   }
   else
   {
      /*******/
      /* GET */
      /*******/

      PostBufferPtr = NULL;
      PostBufferCount = 0;

      /* the "/_/" is a sentinel mapping, see PeriodMunger() */
      if (!strncmp (rdptr->CgiPathInfoPtr, "/soymail/_/", 11))
      {
         PeriodMunger (rdptr);
         return;
      }
   }

   if (gethostname (LocalHostName, sizeof(LocalHostName)) != 0)
      strcpy (LocalHostName, "unknown");

   /* map the request path to the user detail */
   if (rdptr->PublicAccess)
   {
      zptr = (sptr = rdptr->PublicAccessString) +
             sizeof(rdptr->PublicAccessString)-1;
      cptr = rdptr->CgiPathInfoPtr;
      if (*cptr == '/') cptr++;
      while (*cptr && *cptr != '/' && sptr < zptr) *sptr++ = *cptr++;
      *sptr = '\0';

      /* map the request path to the user detail */
      if (!ConfigPublicAccess (rdptr)) ErrorExit (SS$_NOPRIV, FI_LI);
   }

   /* if using soyMAIL's autogenous authentication */
   if (SoyMailConfig.LoginSecret &&
       (rdptr->PrivateAccess ||
        rdptr->PublicAuthorized))
   {
      /* check if any access log is accessable */
      if (VMSnok (status = RequestAccessLog (NULL, NULL)))
         ErrorExit (status, FI_LI);

      if (cptr = LoginVerifyUser (rdptr))
      {
         /****************************************/
         /* login is trying to tell us something */
         /****************************************/

         if (rdptr->PublicAuthorized &&
             SoyMailConfig.PublicNoAccessPage &&
             !strcmp(cptr,SoyMailConfig.PublicNoAccessPage))
         {
            /* no access to this public area */
            status = ReadFileIntoMemory (cptr, &sptr, NULL);
            if (VMSok(status))
            {
               CgiLibResponseHeader (200, "text/html", "%s",
                                     rdptr->LoginSetCookiePtr);
               fputs (sptr, stdout);
            }
            else
               CgiLibResponseError (FI_LI, status, "%s", cptr);
         }
         else
         if (!strcmp(rdptr->CgiRequestMethodPtr, "GET") &&
             isdigit(rdptr->CgiQueryStringPtr[0]))
         {
            /* new mail query where login has failed */
            NewMailLoginFail (rdptr);
            if (*cptr) RequestAccessLog (rdptr, cptr);
         }
         else
         if (!strncmp (rdptr->CgiQueryStringPtr, "preview", 7))
         {
            /* preview query where login has failed - this'll do */
            NewMailLoginFail (rdptr);
            if (*cptr) RequestAccessLog (rdptr, cptr);
         }
         else
         {
            /* invariably from an interactive soyMAIL page */
            LoginPage (rdptr, cptr, PostBufferPtr, PostBufferCount);
            if (*cptr) RequestAccessLog (rdptr, cptr);
         }
         return;
      }

      /******************************/
      /* see description in LOGIN.C */
      /******************************/

      CGIVARNULL (cptr, "FORM_LOGIN_BODY")
      if (cptr && *cptr)
      {
         /*****************************/
         /* what an almighty kludge!! */
         /*****************************/

         /* get the content-type from the original post */
         CGIVARNULL (sptr, "FORM_LOGIN_TYPE")
         if (!sptr) ErrorExit (SS$_BUGCHECK, FI_LI);
         if (strncmp (sptr, "multipart/form-data;", 20))
            ErrorExit (SS$_BUGCHECK, FI_LI);
         sptr += 20;

         /* going to replace this with a saved POST from the form field */
         CgiLibVeeMemFree (PostBufferPtr);

         /* reset the current FORM_ variables using the for-soyMAIL kludge */
         CgiLib__VarList ("!FORM_", "", NULL);

         /* get the saved POST from the form field */
         PostBufferCount = CerealDataIn (cptr, NULL, 0);
         PostBufferPtr =  CgiLibVeeMemCalloc (PostBufferCount);
         if (!PostBufferPtr) ErrorExit (vaxc$errno, FI_LI);
         PostBufferCount = CerealDataIn (cptr, PostBufferPtr, PostBufferCount);
         if (WatchEnabled)
            WatchThis (FI_LI, "BODY !UL bytes\n!AZ", PostBufferCount, PostBufferPtr);

         /* now turn this saved post into form field variables */
         CgiLib__BodyMultipartFormData (PostBufferPtr, PostBufferCount, sptr);
         if (WatchEnabled)
            while ((cptr = CgiLibVarNull ("*")) != NULL)
               WatchThis (FI_LI, "!AZ", cptr);
      }

      if (SoyMailConfig.LoginSecret &&
          rdptr->PublicAuthorized &&
          rdptr->PublicAccessPage[0])
      {
         /**********************/
         /* public access page */
         /**********************/

         status = ReadFileIntoMemory (rdptr->PublicAccessPage, &sptr, NULL);
         if (VMSok(status))
         {
            CgiLibResponseHeader (200, "text/html", "%s",
                                  rdptr->LoginSetCookiePtr);
            fputs (sptr, stdout);
         }
         else
            CgiLibResponseError (FI_LI, status, "%s", rdptr->PublicAccessPage);
         return;
      }
   }
   else
   if (rdptr->PrivateAccess ||
       rdptr->PublicAuthorized)
   {
      /* using HTTP authentication (via server configuration) */
      CGIVAR (cptr, "REMOTE_USER");
      rdptr->UserNameLength = strzcpy (rdptr->UserName, cptr,
                                       sizeof(rdptr->UserName));
      rdptr->RemoteUserLength = strzcpy (rdptr->RemoteUser, cptr,
                                         sizeof(rdptr->RemoteUser));

      /* check if any access log is accessable */
      if (rdptr->UserNameLength)
         if (VMSnok (status = RequestAccessLog (NULL, NULL)))
            ErrorExit (status, FI_LI);
   }

   if (PostBufferPtr)
   {
      /* delayed cleanup due to potential use by LoginPage() */
      CgiLibVeeMemFree (PostBufferPtr);
      PostBufferPtr = NULL;
      PostBufferCount = 0;
   }

   /***********/
   /* request */
   /***********/

   UseCache = IsCgiPlus &&
              SoyMailConfig.ContextCacheSeconds &&
              rdptr->PrivateAccess;

   if (UseCache) ContextCacheSet ();

   if (rdptr->PrivateAccess)
   {
      RequestPrivate (rdptr);
      RequestAccessLog (rdptr, NULL);
   }
   else
   if (rdptr->PublicAccess)
      RequestPublic (rdptr);
   else
      ErrorExit (SS$_BUGCHECK, FI_LI);

   if (UseCache)
      ContextCacheSet ();
   else
   {
      CallMailFileClose (&VmsMailUser);
      CallMailUserClose (&VmsMailUser);
   }

   /* reset watch mode as necessary */
   if (WatchEnabled) WatchThis (FI_LI, NULL);
}

/****************************************************************************/
/*
The mail context cache stores mail user and mail file longword contexts between
requests.  This saves a little time and effort in establishment but the major
saving seems to be in context closure.  So this effectively defers that until
well after the request has concluded as well as keeping contexts established
across requests.  Of course it only applies to WASD CGIplus.
*/ 

void ContextCacheSet ()

{
   int  idx;

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

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

   if (ContextCacheInUse)
   {
      /****************/
      /* update cache */
      /****************/

      ContextCache[ContextCacheIndex].VmsMailFileContext =
         VmsMailUser.VmsMailFileContext;
      ContextCache[ContextCacheIndex].VmsMailUserContext =
         VmsMailUser.VmsMailUserContext;
      ContextCache[ContextCacheIndex].ExpiresSeconds =
         CurrentTimeSeconds + SoyMailConfig.ContextCacheSeconds;

      ContextCacheInUse = FALSE;

      if (ContextCacheDefer) ContextCacheExpire ();
   }
   else
   {
      /****************/
      /* search cache */
      /****************/

      ContextCacheIndex = -1;
      for (idx = ContextCacheCurrent = 0; idx < ContextCacheCount; idx++)
      {
         if (!ContextCache[idx].ExpiresSeconds) continue;
         ContextCacheCurrent++;
         if (!strcmp (RequestData.RemoteUser, ContextCache[idx].UserName))
            ContextCacheIndex = idx;
      }

      if (ContextCacheIndex >= 0)
      {
         /* hit */
         VmsMailUser.VmsMailFileContext =
            ContextCache[ContextCacheIndex].VmsMailFileContext;
         VmsMailUser.VmsMailUserContext =
            ContextCache[ContextCacheIndex].VmsMailUserContext;
      }
      else
      {
         /* miss, look for expired */
         for (idx = 0; idx < ContextCacheCount; idx++)
            if (!ContextCache[idx].ExpiresSeconds) break;
         if (idx < ContextCacheCount)
            ContextCacheIndex = idx;
         else
         {
            /* use 'next' */
            if (ContextCacheNext >= CONTEXT_CACHE_MAX) ContextCacheNext = 0;
            if (ContextCacheCount < CONTEXT_CACHE_MAX) ContextCacheCount++;
            ContextCacheIndex = ContextCacheNext++;
            if (ContextCache[ContextCacheIndex].VmsMailFileContext)
            {
               VmsMailUser.VmsMailFileContext =
                  ContextCache[ContextCacheIndex].VmsMailFileContext;
               ContextCache[ContextCacheIndex].VmsMailFileContext = 0;
               CallMailFileClose (&VmsMailUser);
            }
            if (ContextCache[ContextCacheIndex].VmsMailUserContext)
            {
               VmsMailUser.VmsMailUserContext =
                  ContextCache[ContextCacheIndex].VmsMailUserContext;
               ContextCache[ContextCacheIndex].VmsMailUserContext = 0;
               CallMailUserClose (&VmsMailUser);
            }
         }
         ContextCacheCurrent++;
         strcpy (ContextCache[ContextCacheIndex].UserName,
                 RequestData.RemoteUser);
      }

      ContextCacheInUse = TRUE;
   }
}

/****************************************************************************/
/*
Perform a scan of the mail context cache, expiring and closing the mail context
of those that have expired.  If a request is currently in-progress defer the
scan until the request is concluding and then do it in-line.
*/ 

void ContextCacheExpire ()

{
   static int64  OneMinuteDelta64 = -600000000;

   int  idx, status;

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

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

   if (CgiPlusUsageCount && !ContextCacheCount) return;

   if (ContextCacheInUse)
   {
      /* defer until the end of this request */
      ContextCacheDefer = TRUE;
      return;
   }
   ContextCacheDefer = FALSE;

   sys$gettim (&CurrentTime64);
   CurrentTimeSeconds = decc$fix_time (&CurrentTime64);

   memset (&VmsMailUser, 0, sizeof(VmsMailUser));

   for (idx = 0; idx < ContextCacheCount; idx++)
   {
      if (!ContextCache[idx].ExpiresSeconds) continue;
      if (ContextCache[idx].ExpiresSeconds < CurrentTimeSeconds)
      {
         ContextCache[idx].ExpiresSeconds = 0;
         if (ContextCache[idx].VmsMailFileContext)
         {
            VmsMailUser.VmsMailFileContext =
               ContextCache[idx].VmsMailFileContext;
            ContextCache[idx].VmsMailFileContext = 0;
            CallMailFileClose (&VmsMailUser);
         }
         if (ContextCache[idx].VmsMailUserContext)
         {
            VmsMailUser.VmsMailUserContext =
               ContextCache[idx].VmsMailUserContext;
            ContextCache[idx].VmsMailUserContext = 0;
            CallMailUserClose (&VmsMailUser);
         }
      }
   }

   if (VMSnok (status =
       sys$setimr (0, &OneMinuteDelta64, ContextCacheExpire, 0, 0)))
      exit (status);
}

/****************************************************************************/
/*
Running soyMAIL down.  Close all of the remaining mail contexts.
*/ 

void ContextCacheClose ()

{
   int  idx;

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

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

   if (!ContextCacheCount) return;

   memset (&VmsMailUser, 0, sizeof(VmsMailUser));

   for (idx = 0; idx < ContextCacheCount; idx++)
   {
      if (!ContextCache[idx].ExpiresSeconds) continue;
      VmsMailUser.VmsMailFileContext = ContextCache[idx].VmsMailFileContext;
      VmsMailUser.VmsMailUserContext = ContextCache[idx].VmsMailUserContext;
      CallMailFileClose (&VmsMailUser);
      CallMailUserClose (&VmsMailUser);
   }
}

/****************************************************************************/
/*
For non-EFS (ODS-5) platforms where non-EFS, multiple period syntax is not
supported.

Intended to support URIs associated with TinyMCE and relies on the URI
containing the sentinal "/_/" instead of the more common WASD and soyMAIL -isms
of "/-/".

The WASD mapping rules would be something like:

  redirect  /soymail/-/*  /cgiplus-bin/soymail/soymail/_/*
  pass      /soymail/_/*  /wasd_root/src/soymail/*

Converts directory name periods to underscores, and file names with multiple
periods to file names with trailing single periods and replaced underscores. 
Issues a redirection to the munged URI.
*/ 

void PeriodMunger (REQUEST_DATA *rdptr)

{
   int  pcnt;
   char  *cptr, *sptr, *uptr;

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

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

   cptr = uptr = CgiLibVeeMemCalloc (strlen(rdptr->CgiPathInfoPtr));
   strcpy (uptr, rdptr->CgiPathInfoPtr);
   while (*cptr)
   {
      sptr = cptr;
      for (pcnt = 0; *cptr && *cptr != '/'; cptr++) if (*cptr == '.') pcnt++;
      if (*cptr)
      {
         /* a directory can't have any periods in the name at all */
         if (pcnt) pcnt++;
         /* step over the directory delimiter */
         cptr++;
      }
      /* now munge required periods to underscores */
      while (pcnt-- > 1)
      {
         while (*sptr && *sptr != '.') sptr++;
         *sptr++ = '_';
      }
   }

   CgiLibResponseRedirect ("%s", uptr);
}

/*****************************************************************************/
/*
This is a utility function for use by the soyMAIL administrator to allow an
arbitrary message to be added to a mail repository.  The command-line parameter
must contain at least a message file name.

THE FILE NAME SHOULD SPECIFY A FULL PATH (otherwise, I'm speculating, it
defaults to the same directory as the user mail file is located in).

It optionally may be prefixed by a slash-separated folder name (defaults to
NEWMAIL).  These two optionally may be prefixed by a slash-separated mail file
name (defaults to MAIL).

  /INSERT=<device:[directory]file>
  /INSERT="<folder>/<device:[directory]file>"
  /INSERT="<mail>/<folder>/<device:[directory]file>"

Be aware that quoted parameters retain specified case and Mail folder names are
case-sensitive.  The optional mail file name could be used to specify a trial
repository that allows confirmation of the message file format without
disturbing the primary Mail repository.

The message file must be sequential and have a VAR record format with CR record
attributes.  The format for the content of this file is:

  <form-feed>
  From:<tab>[<from-address>]<nl><tab><date/time>
  To:<tab>[<to-address>]
  CC:<tab>[<cc-address>]
  Subj:<tab>[<subject text>]
  <empty line>
  <body>

"CC:" must be "CC:" not "Cc:"!

Note the leading <form-feed> record that is some sort of sentinal for each (if
multiple) message in the sequential file.  Anything that doesn't look like a
VMS Mail address must be quoted (and for soyMAIL preceded by what looks like a
mail transport).   A form-feed can be inserted using <ctrl>-V then a <ctrl>-L.
A null character (<nl>) can be inserted via TPU using a <ctrl>-V and then a
<ctrl>-2.

An example message file:

  |<ff>
  |From:<tab>MAIL%"info-WASD@vsm.com.au"<nl><tab>04-DEC-2008 14:25:00
  |To:<tab>MAIL%"info-WASD@vsm.com.au"
  |CC:<tab>
  |Subj:<tab>Just an example
  |
  |Return-path: <mark.daniel@wasd.vsm.com.au>
  .
  .
  |--Boundary_(ID_1LVivs429lNuHvWnO2bUIA)--

Message format is further described in the prologue to CallMailCopyFile(). 

Callable Mail only seems to signal some errors in the format of the required
header.  It is possible to add messages (such as those with unacceptable To: or
From: addresses) which subsequently are not accessable via Mail!!  Some care
needs to be exercised in constructing the content of the message file.

This functionality is also available from within the VMS MAIL utility but in a
somewhat less straight-forward fashion.
*/

int InsertMessage (char *CliParam)

{
   static char  JpiUserName [13];
   static struct
   {
      unsigned short  buf_len;
      unsigned short  item;
      unsigned char  *buf_addr;
      void  *ret_len;
   }
   JpiItems [] =
   {
     { sizeof(JpiUserName)-1, JPI$_USERNAME, (unsigned char*)&JpiUserName, 0 },
     { 0,0,0,0 }
   };

   int  cnt, status;
   char  *cptr, *sptr, *zptr;
   char  FileName [256],
         FolderName [128],
         MailFileName [128];

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

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

   cnt = 0;
   for (cptr = CliParam; *cptr; cptr++)
      if (*cptr == '/') cnt++;
   if (cnt > 2) return (CLI$_OVRFLW);

   cptr = CliParam;

   zptr = (sptr = MailFileName) + sizeof(MailFileName)-1;
   if (cnt >= 2) while (*cptr && *cptr != '/' && sptr < zptr) *sptr++ = *cptr++;
   *sptr = '\0';
   if (!MailFileName[0]) strcpy (MailFileName, "MAIL");

   zptr = (sptr = FolderName) + sizeof(FolderName)-1;
   if (*cptr == '/') cptr++;
   if (cnt >= 1) while (*cptr && *cptr != '/' && sptr < zptr) *sptr++ = *cptr++;
   *sptr = '\0';
   if (!FolderName[0]) strcpy (FolderName, "NEWMAIL");

   zptr = (sptr = FileName) + sizeof(FileName)-1;
   if (*cptr == '/') cptr++;
   while (*cptr && sptr < zptr) *sptr++ = *cptr++;
   *sptr = '\0';
   if (!FileName[0]) return (CLI$_INSFPRM);

   status = sys$getjpiw (0, 0, 0, &JpiItems, 0, 0, 0);
   if (VMSnok (status)) return (status);

   for (cptr = JpiUserName; *cptr && *cptr != ' '; cptr++);
   *cptr = '\0';
   strcpy (VmsMailUser.UserName, JpiUserName);
   VmsMailUser.UserNameLength = strlen(JpiUserName);

   status = CallMailGetUserProfile (&VmsMailUser);
   if (VMSnok (status)) return (status);

   strcpy (VmsMailUser.VmsMailFileName, MailFileName);
   VmsMailUser.VmsMailFileNameLength = strlen(VmsMailUser.VmsMailFileName);
   strcpy (VmsMailUser.VmsMailFolderName, FolderName);
   VmsMailUser.VmsMailFolderNameLength = strlen(VmsMailUser.VmsMailFolderName);

   status = CallMailCopyFile (&VmsMailUser,
                              VmsMailUser.VmsMailFileName,
                              VmsMailUser.VmsMailFolderName,
                              FileName);
   if (Debug) fprintf (stdout, "CallMailCopyFile() %%X%08.08X\n", status);

   return (status);
}

/*****************************************************************************/
/*
Get "command-line" parameters, whether from the command-line or from a
configuration symbol or logical containing the equivalent.
*/

GetParameters
(
int argc,
char *argv[]
)
{
   static unsigned long  Flags = 0;

   int  status,
        SkipParameters;
   unsigned short  Length;
   char  ch;
   char  *aptr, *cptr, *clptr, *sptr;
   $DESCRIPTOR (CommandLineDsc, CommandLine);

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

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

   if ((clptr = getenv ("SOYMAIL$PARAM")) == NULL)
   {
      /* get the entire command line following the verb */
      status = lib$get_foreign (&CommandLineDsc, 0, &Length, &Flags);
      if (VMSnok (status)) exit (status);
      (clptr = CommandLine)[Length] = '\0';
   }

   /* the first is CSWS 1.2/1.3, the other two for SWS 2.0 */
   if (getenv ("APACHE$SHARED_SOCKET") != NULL ||
       getenv ("APACHE$PARENT_PID") != NULL ||
       getenv ("APACHE$SERVER_TAG") != NULL)
      SkipParameters = 99;
   else
   /* if OSU environment then skip P1, P2, P3 */
   if (getenv ("WWWEXEC_RUNDOWN_STRING") != NULL)
      SkipParameters = 3;
   else
      SkipParameters = 0;

   aptr = NULL;
   ch = *clptr;
   for (;;)
   {
      if (aptr != NULL && *aptr == '/') *aptr = '\0';
      if (!ch) break;

      *clptr = ch;
      if (Debug) fprintf (stdout, "clptr |%s|\n", clptr);
      while (*clptr && isspace(*clptr)) *clptr++ = '\0';
      aptr = clptr;
      if (*clptr == '/') clptr++;
      while (*clptr && !isspace (*clptr) && *clptr != '/')
      {
         if (*clptr != '\"')
         {
            clptr++;
            continue;
         }
         cptr = clptr;
         clptr++;
         while (*clptr)
         {
            if (*clptr == '\"')
               if (*(clptr+1) == '\"')
                  clptr++;
               else
                  break;
            *cptr++ = *clptr++;
         }
         *cptr = '\0';
         if (*clptr) clptr++;
      }
      ch = *clptr;
      if (*clptr) *clptr = '\0';
      if (Debug) fprintf (stdout, "aptr |%s|\n", aptr);
      if (!*aptr) continue;

      if (SkipParameters)
      {
         /*
             Apache supplies query string keywords on the command line
             to a CGI script.  With Apache the command-line is ignored
             up until the /_APACHE qualifier triggers anything following
             to be processed.             
         */
         if (!strsame (aptr, "/_APACHE", -1))
         {
            SkipParameters--;
            continue;
         }
         SkipParameters = 0;
      }

      /***********/
      /* process */
      /***********/

      if (strsame (aptr, "/DBUG", -1))
      {
         Debug = TRUE;
         continue;
      }

      if (strsame (aptr, "/INSERT=", 8))
      {
         for (cptr = aptr+8; *cptr && isspace(*cptr); cptr++);
         if (!*cptr) exit (CLI$_INSFPRM);
         exit (InsertMessage (cptr));
      }

      if (strsame (aptr, "/LDIF=PURGE", -1))
      {
         CliLdifPurge = TRUE;
         continue;
      }

      if (strsame (aptr, "/REGEX", -1))
      {
         /* kludgy, kludgy, kludgy! */
         SearchRegexCheck (clptr+1);
         exit (SS$_NORMAL);
      }

      if (strsame (aptr, "/VERSION", -1))
      {
         fprintf (stdout, "%%%s-I-VERSION, %s\n%s\n%s",
                  Utility, SoftwareEnv, SOFTWARECR, SOFTWARE_APACHE_2);
         exit (SS$_NORMAL);
      }

      if (*aptr == '/')
      {
         fprintf (stdout, "%%%s-E-IVQUAL, unrecognized qualifier\n \\%s\\\n",
                  Utility, aptr+1);
         exit (STS$K_ERROR | STS$M_INHIB_MSG);
      }

      if (!CliParam1Ptr)
      {
         CliParam1Ptr = aptr;
         continue;
      }
      if (!CliParam2Ptr)
      {
         CliParam2Ptr = aptr;
         continue;
      }

      fprintf (stdout, "%%%s-E-MAXPARM, too many parameters\n \\%s\\\n",
               Utility, aptr);
      exit (STS$K_ERROR | STS$M_INHIB_MSG);
   }
}

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

