-----------------------------------------------------------------------
 Mercury Daemon Interface for Mercury/32 v2.21 and later
-----------------------------------------------------------------------
 Mercury Mail Transport System,
 Copyright (c) 1993-99, David Harris, All Rights Reserved.
-----------------------------------------------------------------------


Contents

1: Introduction
   1.1 - What is a Daemon?
   1.2 - What uses could I have for a Daemon?

2: Technical overview
   2.1 - Basic structure of a Daemon
   2.2 - What do I need to write a Daemon?
   2.3 - Installing and invoking a Daemon

3: Using the Mercury interface
   3.1 - Including header files
   3.2 - The M_INTERFACE structure
   3.3 - Calling functions

4: Advanced topics
   4.1 - DAEMON.INI
   4.2 - "Resident" Daemons
   4.3 - "Global" Daemons
   4.4 - Daemon configuration
   4.5 - Daemon Domains

5: Function reference
   5.1 - General-purpose functions
     get_variable
     is_local_address
     is_group
     parse_address
     extract_one_address
     extract_cqtext
     dlist_info
     send_notification
     get_delivery_path
     get_date_and_time
     write_profile
     module_state

   5.2 - Job-control and management functions
     ji_scan_first_job
     ji_scan_next_job
     ji_end_scan
     ji_open_job
     ji_close_job
     ji_rewind_job
     ji_dispose_job
     ji_process_job
     ji_delete_job
     ji_abort_job
     ji_get_job_info
     ji_create_job
     ji_add_element
     ji_add_data
     ji_get_data
     ji_get_next_element
     ji_set_element_status
     ji_set_element_resolvinfo
     ji_set_diagnostics
     ji_get_diagnostics
     ji_increment_time
     ji_get_job_by_id
     ji_get_job_times

   5.3 - Network and user database query functions
     get_first_group_member
     get_next_group_member
     end_group_scan
     is_valid_local_user
     is_group_member
     get_first_user_details
     get_next_user_details
     get_user_details
     end_user_scan
     read_pmprop
     change_ownership
     begin_single_delivery
     end_single_delivery
     create_object
     verify_password
     set_password

   5.4 - Miscellaneous functions
     mercury_command
     get_date_string
     rfc822_time
     rfc821_time
     select_printer
     print_file

   5.5 - File I/O and parsing routines
     fm_open_file
     fm_open_message
     fm_close_message
     fm_gets
     fm_getc
     fm_ungetc
     fm_read
     fm_getpos
     fm_setpos
     fm_get_folded_line
     fm_find_header
     fm_extract_message
     parse_header
     mime_prep_message
     parse_mime
     free_mime
     fake_imessage
     decode_mime_header
     encode_mime_header
     encode_base64_str
     decode_base64_str

   5.6 - Message composition routines
     om_create_message
     om_dispose_message
     om_add_field
     om_add_attachment
     om_write_message
     om_send_message
     Notes on composing messages

   5.7 - Statistics and logging interface
     st_register_module
     st_unregister_module
     st_create_category
     st_remove_category
     st_set_hcategory
     st_set_category
     st_get_next_module
     st_get_next_category
     st_get_category_data
     st_export_stats
     logstring
     logdata


Appendix A: Technical issues



-----------------------------------------------------------------------
 1: Introduction
-----------------------------------------------------------------------

1.1 - What is a Daemon?

A Mercury/32 "Daemon" (a term inherited from the unix world) is a program
that provides extended services within the Mercury/32 Mail Transport
System. Daemons are associated with a particular e-mail address, and
when a message is sent to that address, Mercury invokes the Daemon to
process it.

A Daemon has access to extensive internal Mercury services, and can
easily perform complex operations such as address parsing and message
creation using simple function calls.

Daemons are implemented as Windows DLLs that (in their simplest form)
export a single function. There are no restrictions on what they can do,
and they have access to the full range of Windows programming services.


1.2 - What uses could I have for a Daemon?

The most obvious use for a Daemon is to perform custom processing on mail
messages; for instance, you might create a Daemon that accepts orders by
e-mail, checks their validity, verifies a credit card number, then
submits the order to a central database for processing. Another example:
you might create a Daemon that sends faxes: when a message arrives, the
Daemon looks for a fax number on the first line, then calls some other
service on the workstation and asks it to send the remainder of the mail
message as a fax to that number.

The only real limit on the uses a Daemon might have is your imagination
and the extent to which you are prepared to do some Windows programming
to realise what you have imagined.


-----------------------------------------------------------------------
 2: Technical Overview
-----------------------------------------------------------------------

2.1 - Basic structure of a Daemon

In its simplest form, a Daemon is a simple 32-bit Windows DLL that
exports a single function, called "daemon". To invoke the Daemon, Mercury
loads the DLL and calls the "daemon" function with a reference to the
mail message (or "job"), a parameter block, and the delivery address that
triggered the call (so the same Daemon can be attached to multiple e-mail
addresses and can distinguish between them as required).

The prototype for the "daemon" function is as follows:

   short _export daemon (void *job, M_INTERFACE *m, char *address,
      char *parameter);

   "job"  is a handle to the mail job that triggered the call to the
       daemon. Using this handle, you can access the data in the message,
       by passing it to functions like "ji_get_data" (see below). You
       must not close or delete this job. Note: on entry to your daemon
       function, this job will be open - you should not attempt to open
       it using "ji_open_job", nor should you close it.

   "m" points to an M_INTERFACE structure: this structure contains pointers
       to various Mercury internal functions that your Daemon can use to
       parse addresses, query information and send mail.

   "address" points to the address that triggered this invocation of the
       Daemon. This allows a single Daemon to service multiple addresses,
       and to adjust its behaviour depending on which address it is
       servicing at any given time. You must not alter the contents of
       this string in any way.

    "parameter" points to any optional data specified in the Daemon's
       alias entry (see below for more information on optional
       parameters). This parameter will point to an empty string ("") if
       there are no parameters. The maximum length of parameter data is
       128 characters.

When the "daemon" function returns control to Mercury, Mercury will
unload the Daemon's DLL and delete the job - no further attempt will be
made to deliver it. The return value from the "daemon" function is
currently ignored and must be set to 0. The return value may be
meaningful in future.


2.2 - What do I need to write a Daemon?

A Daemon is simply a standard Windows DLL that exports a single entry
point; as such, you can use most standard tools to create one. The
functions exported to the Daemon via the M_INTERFACE structure all use
the C calling convention and expect C-type parameters (so, strings are
NUL-terminated arrays of characters).

The most logical tools for developing Daemons are Borland C++ v4.5 or
later, Borland C++/Builder, or Microsoft Visual C++. The sample code
provided with this documentation has been developed and tested using
Borland C++ v5.02.


2.3 - Installing and invoking a Daemon

Installing a Daemon so that Mercury can invoke it requires the creation
of an alias in a special form. The reason Daemons can only be invoked
via an alias is to ensure that only approved Daemons are run, for
security reasons. Aliases can be easily created and maintained from
within Mercury/32, using the "Aliases" option on the "Configuration"
menu.

The alias must be of the following form:

   daemon_address@host.domain == daemon:path_to_dll[;parameter]

"daemon_address" should be whatever address will invoke the Daemon, while
"path_to_dll" should be the fully-qualified path to the Daemon's DLL
file. If your Daemon needs a parameter passed to it when its "daemon"
function is invoked, you can specify that parameter by placing a
semicolon after the filename, followed by the text you want your Daemon
to receive. The maximum length of the parameter is 128 characters.

Example: you wish to install a Daemon called "cookie"; the DLL file for
the Daemon is "c:\mercury\daemons\cookie.dll", and your domain is
"biscuit.com". You would create the following alias:

   cookie@biscuit.com == daemon:c:\mercury\daemons\cookie.dll

If the Daemon's DLL file is found in the same directory as MERCURY.EXE,
then you can omit the path from the DLL's file specification.

Daemons can add aliases to or remove aliases from the system alias file
by themselves: an elegantly-written Daemon would provide a configuration
interface that automates the process of adding the aliases, rather than
relying on the user to do it.


-----------------------------------------------------------------------
 3: Using the Mercury interface
-----------------------------------------------------------------------

3.1 - Including header files

In order to use the Mercury Daemon interface, you will need to add the
following line near the top of your C or C++ source file:

#include "daemon.h"

Note that you must include this line *after* you have included the
standard "windows.h" master header file.


3.2 - The M_INTERFACE structure

When your Daemon is invoked, Mercury passes it a large structure called
an M_INTERFACE. This structure contains some static data, and a number of
pointers to internal Mercury functions that your Daemon can call.

   "dsize"   The "dsize" parameter is the size of the M_INTERFACE
      structure in bytes. A Daemon can use this as part of a version-
      checking process.

   "vmajor"  The major version number of the copy of Mercury that is
      running. For instance, for Mercury v2.15, this value will be "2".

   "vminor"  The minor version number of the copy of Mercury that is
      running. For instance, for Mercury v2.15, this value will be "15".


3.3 - Calling functions

The remaining variables in the M_INTERFACE structure are pointers to
functions within Mercury that the Daemon can call to access core
services. To call one of these functions, simply use the pointer as if it
were a normal function - so, if you want to call the "get_variable"
function to retrieve the GV_QUEUENAME variable, you would use this line
of code

   char *str
   str = (char *) (m->get_variable (GV_QUEUENAME));

Note the C cast to a (char *) - this simply suppresses compiler warnings,
because the "get_variable" function always returns its results as a DWORD
value.


-----------------------------------------------------------------------
 4: Advanced topics
-----------------------------------------------------------------------

4.1 - DAEMON.INI

For advanced Daemon functions such as Residency and Configuration,
Mercury scans a file called DAEMON.INI for parameters. DAEMON.INI uses a
slightly different syntax from MERCURY.INI, in that it uses "=" as a
separator between keyword and parameter instead of the ":" used in
MERCURY.INI. This design difference is intended to allow installers and
Daemons to use the Windows WritePrivateProfileString and
GetPrivateProfileString API functions to update and access the file.


4.2 - "Resident" Daemons

There may be occasions when a Daemon operates better by remaining in
memory at all times, instead of being loaded and unloaded as required.
Internally, a "Resident" Daemon is no different from a normal Daemon,
except that it can optionally export a "startup" function that Mercury
will call the when the Daemon is loaded.

To install a "Resident" Daemon, add a [Daemons] section to a file called
DAEMON.INI in the same directory as MERCURY.EXE, and include a name and the
full path to your Daemon's DLL on a line in that section.

   Example: your Daemon is called "Cookie Daemon" and is located in
   C:\MERCURY\DAEMONS\COOKIE.DLL

   [Daemons]
   Cookie Daemon = c:\mercury\daemons\cookie.dll

Mercury will load the Daemon at startup and will not unload it until
exiting. The Daemon can export a function with the following prototype:

   short _export startup (M_INTERFACE *mi, UINT_32 *flags,
      char *name, char *param)

If this function is present in the DLL, Mercury will call it with an
Interface block as soon as the DLL is loaded. Note that the Interface
block is not persistent - if you need to store it for later use, you must
allocate your own storage and make a copy of the structure in it.

The "flags" parameter to "startup" is a location where the Daemon can
return certain indicators about itself. At the time of writing this
specification, no flag values are defined - you should write 0 into the
location pointed to by the "flags" pointer.

The "name" parameter is the name string defined for the Daemon in
DAEMON.INI - you will typically use this if you have to create a message
box or error dialog.

The "param" parameter to "startup" is an optional string, presumably
containing configuration or run data for your Daemon. You can specify a
parameter by placing a semicolon and the parameter after the name of the DLL
in the entry in the [Daemons] section; so, using our Cookie Daemon above
as an example, if you wanted to pass the parameter "autocookie" to the
startup function, you would have an entry like this in DAEMON.INI:

   [Daemons]
   Cookie Daemon = c:\mercury\daemons\cookie.dll;autocookie

If a Daemon has no parameters, an empty string ("") will be passed in
"param". The maximum length of parameter data is 128 characters.

The "daemon" function of a Resident Daemon is called in exactly the same
way as it would be for a normal Daemon - the only difference is that the
Daemon's DLL is not loaded and unloaded as part of the process. A
Resident Daemon will commonly create a copy of the job in a file using
the "fm_extract_message" function, then spin off a thread to process the
file at its leisure.

Resident Daemons may fire threads at will if they wish to perform routine
processing (this is usually done from the "startup" function). All the
functions in the M_INTERFACE parameter block are thread-safe.

Resident Daemons may export a function called "closedown": if they do,
Mercury will call it as part of its shutdown process prior to unloading
the resident Daemon. The function should have this protoype:

   short _export closedown (M_INTERFACE *m, DWORD code,
      char *name, char *param);

The "code" parameter is reserved for future use and should be ignored
at present. The return from this function is currently unused but must
be set to 0. Only Resident Daemons (including "Global" Daemons) will
have this function called.

The "name" parameter is the name string defined for the Daemon in
DAEMON.INI - you will typically use this if you have to create a message
box or error dialog.

The "param" parameter is the same as was passed to the "startup" function
- see above for more details. If a Daemon has no parameters, an empty
string ("") will be passed in "param".  The maximum length of parameter
data is 128 characters.


4.3 - "Global" Daemons

A "Global" Daemon is a specialized form of Resident Daemon that is
passed all messages processed by the Mercury core module. Global Daemons
are called before any other processing is done on the job, and can
instruct Mercury to process, delete or defer a job through their return
value. Examples of uses for Global Daemons include a Daemon that scans
all incoming and outgoing mail for viruses, or a Daemon that makes
archival copies of all incoming and outgoing mail for auditing purposes.

To install a "Global" Daemon, add a [Global Daemons] section to a file
called DAEMON.INI in the same directory as MERCURY.EXE, and include a
name and the full path to your Daemon's DLL on a line in that section.

   Example: your Daemon is called "Spam Killer" and is located in
   C:\MERCURY\DAEMONS\SPAMKILL.DLL

   [Global Daemons]
   Spam Killer = c:\mercury\daemons\spamkill.dll

Global Daemons are always Resident Daemons - see the preceding section
for information on this. When a Global Daemon's "daemon" function is
called, the "address" parameter is always set to "[Global]".

A Global Daemon may not delete a job directly, but it can return the
following values to Mercury to control the processing of the job:

   0  - Process the job normally
   1  - Delete the job without further processing
   2  - Defer the job for the system defer time

If a Global Daemon instructs Mercury to delete a job, the job is deleted
at once, without further ado. No error notification is sent, nor is there
any indication other than a comment on the core module console display
that the job has been killed.


4.4 - Daemon configuration

Daemons may wish to add a configuration option to the Mercury
"Configuration" menu to allow the user to change settings or otherwise
control the behaviour of the Daemon. To do this, add a [Daemon_Config]
section to a file called DAEMON.INI in the same directory as MERCURY.EXE,
and include a name and the full path to your Daemon's configuration DLL
on a line in that section. The name is used to create your Daemon's
configuration menu line.

   Example: your Daemon's configuration module is called "Cookie Daemon"
   and is located in C:\MERCURY\DAEMONS\COOKIECF.DLL

   [Daemon_Config]
   Cookie Daemon = c:\mercury\daemons\cookiecf.dll

The configuration DLL can be either your Daemon's main processing DLL, or
a subsidiary DLL that only performs configuration functions - the choice
of which method to use will depend on your needs.

Mercury will add your Daemon's name to its Configuration menu, and when
the option is selected, will load your DLL and look for a function with
the following prototype:

   short _export configure (M_INTERFACE *mi, char *name,
      char *param);

"M_INTERFACE" is a regular parameter block. "name" is the name defined
in the [Daemon_Config] section (this allows the same DLL to service
multiple functions keyed on the name).

The "param" parameter to "config" is an optional string, presumably
containing configuration or run data for your Daemon. You can specify a
parameter by placing a semicolon and the parameter after the name of the
DLL in the entry in the [Daemons] section; so, using our Cookie Daemon
above as an example, if you wanted to pass the parameter "autoconfig" to
the config function, you would have an entry like this in DAEMON.INI:

   [Daemon_Config]
   Cookie Daemon = c:\mercury\daemons\cookiecf.dll;autoconfig

If a Daemon has no parameters, an empty string ("") will be passed in
"param". The maximum length of parameter data is 128 characters.

If your DLL refers to a Resident Daemon, Mercury will look for the
configuration function in the loaded copy and will not load the DLL
again. If your DLL needs to be loaded to perform configuration, its
"startup" function will not be called.

Both resident and non-resident Daemons can use configuration services if
they wish. What the "configure" function does when called is up to the
individual developer, but it is normal for it to show a dialog allowing
the user to change its configuration options.


4.5 - Daemon Domains

Occasionally, you may wish to have a Daemon that services all mail sent
to a particular subdomain. As an example, imagine a fax server Daemon
that treats the address portion as a fax number: for a server of this
kind, you would want to be able to send any "username" and have it
processed by the Daemon.

To create a "Daemon Domain" of this kind, select the "Mercury Core Module"
configuration option on the "Configuration" menu, and locate the group of
controls near the bottom of the dialog labelled "Domains recognized as
local by this server". Create a new domain entry where the "Domain name"
portion is the domain name to be handled by your Daemon, and the
"Host/server" portion is "daemon:path_to_dll". Once this domain entry has
been created, all mail sent to any user at the domain you have defined
will result in the Daemon being invoked.

   Example: you have a fax server Daemon, C:\MERCURY\FAX.DLL; you want
   all mail addressed to "fax.biscuit.com" to be passed to this Daemon.
   You would create the following domain definition:

   Host/Server                  Domain name
   daemon:c:\mercury\fax.dll    fax.biscuit.com

Note that this kind of operation will almost always require special name
server entries called "MX Entries" to advertise the domain name - contact
your service provider for more details on creating MX entries.


-----------------------------------------------------------------------
 5: Function reference
-----------------------------------------------------------------------

5.1 - General-purpose functions

   -------------------------------
   DWORD get_variable (int index);
   -------------------------------

   Returns the value of an internal Mercury variable. The following
   values can be passed for "index":

      GV_QUEUENAME (1)       (Returns "char *")
         The name of the directory where Mercury's queue manager is
         looking for jobs. The return value is a (char *) pointing to
         a path, which may be in either drive letter or UNC format.

      GV_SMTPQUEUENAME (2)   (Returns "char *")
         The name of the directory where Mercury's queue manager is
         looking for outgoing mail jobs. This variable is not meaningful
         under current versions of Mercury and should not be used.

      GV_MYNAME (3)          (Returns "char *")
         The Internet name for this copy of Mercury (the domain portion
         it will use when forming addresses).

      GV_TTYFONT (4)         (Returns "HFONT")
         A handle to the font Mercury is currently using to draw text
         in console listings.

      GV_MAISERNAME (5)      (Returns "char *")
         The name of the Mercury mail server (usually "MAISER")

      GV_FRAMEWINDOW (6)     (Returns "HWND")
         The handle to the Mercury Frame window: Daemons can use this
         value to send messages to the Mercury core process.

      GV_SYSFONT (7)         (Returns "HFONT")
         A handle to the font Mercury is using by default to draw
         controls and general dialog text.

      GV_BASEDIR (8)         (Returns "char *")
         Returns the directory from which MERCURY.EXE was run.

      GV_SCRATCHDIR (9)      (Returns "char *")
         Returns a temporary working directory if one has been defined
         in Mercury. If no temporary directory has been defined, this
         value returns the same value as "GV_BASEDIR".

   Daemons must treat all returned values as read-only and must not
   attempt to modify them.


   --------------------------------------------------------------
   int is_local_address (char *address, char *uic, char *server);
   --------------------------------------------------------------

   Determine whether or not the address in "address" refers to a local
   account (i.e, one to which Mercury can complete final delivery).

   "uic"      Receives the local username matching the address when
              the function is successful. Allocate at least 256
              characters for this string.

   "server"   Receives network-specific information associated with the
              local user when the function is successful. This value may
              have to be passed to other functions. Allocate at least
              256 characters for this string.

   Returns:   2 if the address is non-local but served by an alias
              1 if the address is local;
              0 if the address is not local;
             -1 on error (local domain but no such user)

   This function resolves aliases and synoyms, and performs any
   necessary network lookups to validate the address.


   ---------------------------------------------------------------
   int is_group (char *address, char *host, char *gname);
   ---------------------------------------------------------------

   Determine whether the address contained in "address" refers to a valid
   group to which Mercury can perform delivery.

   "address"  The simplified form of the address, with no domain portion
              (so, pass "everyone", not "everyone@host.domain").

   "host"     Receives network-specific host information when the
              function is successful. You may need to pass this value
              to other functions.

   "gname"    Receives the name of the group on success.

   Returns    1 if the address refers to a known group
              0 if the address does not refer to a known group


   ----------------------------------------------------------
   int parse_address (char *target, char *source, int limit);
   ----------------------------------------------------------

   Reduce an RFC822 address to its simplest form, by discarding any
   textual components it contains.

   Example
      "David Harris" (Pegasus Mail Author) <david@pmail.gen.nz>
   would be reduced by this function to
      david@pmail.gen.nz

   "source"   points to the address that is to be reduced. This string
              is not changed as part of the process.

   "target"   points to the location where the reduced address should be
              written. You may pass the same value as "source" if you
              want to overwrite the address with the reduced form.

   "limit"    the maximum length of the resulting string. You should
              pass the allocated size of "target" less one.

   Returns    1 if the reduction was successful
              0 on error (the address was malformed in some way)


   ---------------------------------------------------------------
   int extract_one_address (char *dest, char *source, int offset);
   ---------------------------------------------------------------

   Given a string potentially containing multiple addresses separated
   by commas, extract the next address from the string into "dest".
   The first time you call this function, pass zero for "offset"; for
   each subsequent call, pass the value returned by the previous call
   to the function, until it returns 0.

   "dest"     receives the next address from the string

   "source"   The string containing comma-separated addresses

   "offset"   0 on the first call, the return from the previous call
              to the function on subsequent calls.

   Returns:   > 0 on success ("dest" contains a valid address)
              0 when no more addresses exist ("dest" is invalid)


   --------------------------------------------------------
   void extract_cqtext (char *dest, char *source, int len);
   --------------------------------------------------------

   Given a string containing a single address, strip out the address
   part and return only the extra textual information.

   Example:
      "David Harris" <david@pmail.gen.nz>
   will be reduced by this function to
      David Harris

   "dest"     receives the reduced textual form

   "source"   points to the address to reduce

   "len"      the maximum number of characters to write into "dest".

   Returns:   Nothing.


   ------------------------------------------------------
   int dlist_info (DLIST *dlist, char *lname, int number,
      char *address, char *errbuf, LIST *modlist);
   ------------------------------------------------------

   Returns information about the distribution list named in "lname".
   For information on the DLIST data structure, see "daemon.h".

   "dlist"    points to a DLIST data structure into which the data
              for the list should be written.

   "lname"    points to the simple name of the list (no domain part).

   "number"   if "lname" is NULL, then this variable is used to
              determine which list to retrieve. Calling functions
              can iterate through all the lists on the server by
              setting "lname" to NULL then repeatedly incrementing
              this variable in successive calls.

   "address"  if non-NULL, contains a simplified address form which
              should be compared with the list of moderators for the
              list. If the address matches any moderator in the list,
              the "matched" field of the DLIST structure will be set
              to 1.

   "errbuf"   receives a textual error message if the function fails.
              Allocate at least 128 characters for this variable.

   "modlist"  points to a LIST data structure that receives a list
              of all the moderators defined for the list. This value
              can be NULL.

   Returns:   0 on success
             -1 on failure

   Note the non-standard return value convention for this function.


   -------------------------------------------------------------------
   void send_notification (char *username, char *host, char *message);
   -------------------------------------------------------------------

   Send a short, one-line message directly to the given user.

   "username" a username, as returned by "is_local_user"

   "host"     network-specific host information as returned by
              "is_local_user".

   "message"  A message of up to 65 characters to send to the user

   Returns    Nothing

   This function only works if a network enabler supporting broadcast
   messages (such as the NetWare 3.x and 4.x enablers) has been installed
   and is currently loaded in Mercury/32.


   ---------------------------------------------------------------
   int get_delivery_path (char *path, char *username, char *host);
   ---------------------------------------------------------------

   Retrieve the delivery path for the specified user/host combination.
   "user" and "host" should be the values returned by "is_local_address".

   "path"     Receives the directory in which files should be created
              for final delivery to the specified user. The string will
              not end in "\" or "/". Allocate at least 256 characters
              for this string

   Returns:   1 if "path" contains a valid delivery path
              0 on error ("path" is invalid)


   ---------------------------------
   int get_date_and_time (BYTE *tm);
   ---------------------------------

   Get the current date and time, querying the file server for the
   information if possible. The information is written into a seven
   byte structure laid out as follows:

      Byte 0  - Years since 1900 (i.e, 2000 == 100)
      Byte 1  - Month (ie, January == 1)
      Byte 2  - Day (1 .. 31)
      Byte 3  - Hour (0 - 24)
      Byte 4  - Minute (0 - 59)
      Byte 5  - Second (0 - 60)
      Byte 6  - Day of week (Sunday == 0)

   This function will obtain the date and time from the local workstation
   if no server connection is available to provide it.

   Returns:   1 if the time and date were correctly retrieved
              0 on system error.


   -----------------------------------------------
   int write_profile (char *section, char *fname);
   -----------------------------------------------

   Write a profile section into MERCURY.INI. "section" should contain
   the name of the section to write, enclosed in square brackets (so,
   if you want to write a section called "CookieServ", you would pass
   "[CookieServ]" as the "section" parameter). "fname" should be a full
   path to a file containing the data to write into the section. The
   data is written exactly as it appears and may use any syntax you
   wish, although it is conventional to use "keyword : parameter".
   The data in the file is expected to be line-oriented, and no single
   line may exceed 254 characters.

   Returns:   1 on success
              0 on failure ("fname" is not accessible)


   -----------------------------------------------------------
   int module_state (char *modname, int set_value, int state);
   -----------------------------------------------------------

   Query or set the state for a protocol module. "modname" should point
   to a string identifying the module name of the module whose state is
   to be queried or set. If "set_value" is non-zero, then this function
   call will attempt to set the module's state to the value contained
   in "state". If "set_value" is zero, then "state" is ignored, and the
   module's current state is returned.

   A module's state is a bitmap where the low sixteen bits are reserved
   and the high sixteen bits can be defined by individual modules for
   whatever purpose they wish. The reserved bits have the following
   possible values:

      Bit 0:  Online state, 1 = online, 0 = offline.
      Bit 1:  ("get" only) 1 = busy, 0 = idle.

   Returns:  -1:  No such module
             -2:  Module does not support this operation
         < -255:  Module-specific error return value
           >= 0:  Previous state (success)



5.2 - Job control and interface functions

The functions in this section allow you to create, scan and manipulate
mail jobs in the Mercury queue.


   ----------------------------------------------------------
   void *ji_scan_first_job (int type, int mode, void **data);
   void * ji_scan_next_job (void **data);
   void ji_end_scan (void **data);
   ----------------------------------------------------------

   Routines for enumerating jobs in the queue. First, call
   "ji_scan_first_job" and if it returns a non-NULL value, call
   "ji_scan_next_job" until NULL is returned. A non-NULL return value
   is the job handle for the job that is found - use this handle in
   calls to other job management functions.

      "type"   Can have any one of the following values:
               JT_GENERAL    Messages for local delivery
               JT_OUTGOING   Messages scheduled for off-server delivery
               JT_ANY        Any type of message


      "mode"   Used to select jobs in particular states - choose from:
               JE_READY      Messages ready for processing
               JE_COMPLETED  Messages where all processing is finished
               JE_FAILED     Messages with delivery failures pending
               JE_ANY        Messages in any state

   The "data" parameter is used by these routines to store state
   information, and should be passed as part of each call. If
   "ji_scan_first_job" returns a non-NULL value, then you *must* call
   "ji_end_scan" when you have finished scanning with jobs, even if you
   do not completely iterate through all the jobs in the queue.

   None of these routines actually opens the job handle - you must call
   ji_open_job, passing the job handle, if you need to access the data in
   the job or modify its settings.

   Returns:    NULL if no further jobs exist
               non-NULL (the job handle) on success


   ----------------------------------
   int ji_open_job (void *jobhandle);
   ----------------------------------

   Open a job. You must call this function before calling any function
   that accesses the job's data or changes the job's settings. The job
   is opened and locked, which will prevent it from being processed
   during normal polling operations, and will prevent other processes
   from opening it.

   The job handle passed to this call should be a value returned by
   "ji_scan_first_job", "ji_scan_next_job" or "ji_create_job".

   Returns:    1 if the job was opened successfully.
               0 on failure.


   ----------------------------------
   int ji_close_job (void *jobhandle);
   ----------------------------------

   Close a job opened with "ji_open_job". The job is unlocked and
   released for normal processing. Other processes may access the job
   once this routine has been called.

   Calling this routine forces the job's overall status to be updated
   (so, a job may change from JE_READY to JE_COMPLETED after this
   function has been called).

   Returns:    1 on success
               0 on failure (job invalid).


   ------------------------------------------------
   void ji_rewind_job (void *jobhandle, int flags);
   ------------------------------------------------

   "Rewind" a job - reposition its file pointer at 0. "flags" can have
   either of two values, "JR_CONTROL", in which case the control
   information for the job is reset to the first address element, or
   "JR_DATA", in which case the data associated with the file is set
   for reading from the start.

   Example: say you are scanning through the addresses to which a message
      should be sent using "ji_get_next_element", and you strike a
      condition that means that you need to start processing from the
      first address element in the message again, you would call this
      function with the "JR_CONTROL" parameter.


   -------------------------------------
   int ji_dispose_job (void *jobhandle);
   -------------------------------------

   Call this function when you have finished working with a job handle
   returned by ji_scan_first_job, ji_scan_next_job, or ji_create_job.
   This routine deallocates memory associated with the job - it does not
   delete or in any other manner change the actual disk structures
   associated with the job. This function must be called for every
   non-NULL return value from the functions listed above.

   Returns:    1 on success
               0 on failure (job invalid).


   -------------------------------------
   int ji_process_job (void *jobhandle);
   -------------------------------------

   Indicates to the job manager that a formal attempt to process the
   job has begun. The retry count for the job is incremented and (if
   applicable) the last retry time is updated. Daemons should seldom
   if ever have any cause to call this function.

   Returns:    1 on success
               0 on failure (invalid or closed job handle)


   ------------------------------------
   int ji_delete_job (void *jobhandle);
   ------------------------------------

   Permanently remove a job from the queue. This routine deletes all
   files associated with the job (including diagnostic files) and then
   discards the job handle. It is invalid to access the job handle after
   it has been passed to this routine.

   You should not call ji_dispose_job for handles passed to this routine.

   Returns:  1 on success
             0 on failure (invalid job handle)


   ----------------------------------------------
   int ji_abort_job (void *jobhandle, int fatal);
   ----------------------------------------------

   Abort processing a job. If the job was created using ji_create_job,
   then it is discarded, otherwise its retry count is incremented by the
   system default retry increment, the job is closed, and the job handle
   is invalidated. It is an error to use or access the job handle after
   calling this routine.

   If "fatal" is non-zero, then the job is unilaterally failed (normally,
   a job is only marked as failed if any of its addresses cannot be
   delivered - setting this flag will fail even a job where all the
   recipients have successfully received the mail).

   You should not call ji_dispose_job for handles passed to this routine.

   Returns:  1 on success
             0 on failure (job handle was invalid)


   ---------------------------------------------------
   int ji_get_job_info (void *jobhandle, JOBINFO *ji);
   ---------------------------------------------------

   Get information about the specified job. The fields in the JOBINFO
   structure are filled out with information from the current element
   in the message's control stream.

      typedef struct
         {
         int structlen;      //  Size of this structure
         char jobstatus;     //  JS_READY, JS_COMPLETE or JS_FAILED
         long jobflags;      //  Mercury internal processing flags
         char status;        //  As jobstatus but for current element only
         char *from;         //  Envelope address for message (read-only)
         char *to;           //  Recipient address for current element
         long dsize;         //  Total RFC822 size of the message
         long rsize;         //  Number of bytes read since last rewind
         int total_rcpts;    //  Total recipients for message
         int total_failures; //  Total failed recipients to date
         int total_retries;  //  Total recipients marked for retry
         char ip1 [16];      //  Primary IP address for current element
         char ip2 [16];      //  Secondary IP address for "   "    "
         char ip3 [16];      //  Third-choice IP address for  "   "   "
         char ip4 [16];      //  Fourth-choice IP address for "   "   "
         char jobid [20];    //  Unique identifier for message
         } JOBINFO;

   Returns:  1 on success
             0 on failure


   ----------------------------------------------------------------------
   void *ji_create_job (int type, char *from, unsigned char *start_time);
   ----------------------------------------------------------------------

   Create a job in the mail queue.

   "type" can be either JT_GENERAL, or JT_OUTGOING. You should usually
      use "JT_GENERAL" unless you are explicitly creating a job which is
      to be processed directly by the MercuryC SMTP client.

   "from" is the address Mercury should write in the envelope of the
      message.

   "start_time" is a date and time in the 7-byte format described under
      "get_date_and_time", which specifies the earliest time at which the
      job can be processed. If NULL, the job can be processed as soon as
      it has been closed.

   The job handle returned by this function can be passed to any other
   job management function in order to manipulate the queue job.

   Returns:   non-NULL on success (the job handle)
              NULL on failure.


   ----------------------------------------------------
   int ji_add_element (void *jobhandle, char *address);
   ----------------------------------------------------

   Add an address to a job created using "ji_create_job". "address"
   should be the full e-mail address of a single recipient for the
   message. You should call this function for each address to which the
   message should be sent.

   Returns:   1 on success
              0 on failure (invalid job handle)


   ----------------------------------------------
   int ji_add_data (void *jobhandle, char *data);
   ----------------------------------------------

   Add data for the message body to a job created using "ji_create_job".
   You should call this function repeatedly for the headers and text of
   the message until all the data has been written. This function does
   not modify the data in any way - it is up to you to ensure that line
   endings are correct (CR/LF pairs) and that the data conforms to the
   standards for Internet mail. There is a clear expectation that the
   data for the message will be passed to this function a line at a time.

   Note: When you use this function to build an outgoing mail message,
   you are effectively building the message exactly as it will be sent -
   it is up to you to add all the necessary RFC822 headers, the blank
   line that separates them from the message body, and the message body
   itself.

   Returns:   1 on success
              0 on failure (invalid job handle)


   --------------------------------------------------------------
   char *ji_get_data (void *jobhandle, char *buffer, int buflen);
   --------------------------------------------------------------

   Get the next line of data from the current job. "buffer" should be
   allocated to be at least 1024 characters in order to comply with
   RFC821, but any length will be honoured. The line returned will
   usually end in a CRLF pair, but may not if the buffer was too
   small to accommodate the entire line.

   Returns:  "buffer" on success
              NULL on failure (end of data)


   -----------------------------------------------------
   char *ji_get_next_element (void *jobhandle, int type,
      JOBINFO *jobinfo);
   -----------------------------------------------------

   Get the next element from the job. Information about the element
   is returned in "jobinfo". "type" can be JE_ANY for any type of entry,
   JE_READY for the next entry marked as "ready for processing", or
   JE_FAILED for the next entry marked as failed.

   Returns:   The e-mail address associated with the entry on success
              NULL on failure, or no more elements.


   -----------------------------------------------------
   int ji_set_element_status (void *jobhandle, int mode,
      unsigned char *date);
   -----------------------------------------------------

   Set the status fields of the current job element. "mode" can be one
   of the following values:

      JS_COMPLETED   -  "date" is ignored
      JS_FAILED      -  "date" is ignored
      JS_RETRY       -  "date" is used for requeuing if non-NULL
      JS_PENDING     -  "date" is ignored

   "JS_PENDING" status indicates a status that should be reset to
   "JS_RETRY" if a job is aborted: it is set by the SMTP client
   module when the remote SMTP server has accepted a RCPT TO: line
   for the address, and is needed so that the job can be "rolled
   back" if there's a subsequent network error.

   Returns:  1 on success
             0 on failure (invalid job handle or element)


   ----------------------------------------------------------------------
   int ji_set_element_resolvinfo (void *jobhandle, char *ip1, char *ip2);
   ----------------------------------------------------------------------

   Set the resolver information fields of the current job element. "ip1"
   and "ip2" point to ASCII versions of IP addresses for servers that
   should be used to deliver the element within the job. Daemons should
   never need to use this function.

   Returns:  1 on success
             0 on failure (invalid job handle or element)


   ------------------------------------------------------------------
   int ji_set_diagnostics (void *jobhandle, int forwhat, char *text);
   ------------------------------------------------------------------

   Set the diagnostic field of the current job element to the text
   contained in "text". It is legal (and probably even essential)
   to call this function repeatedly; each time it is called, a line
   is added to the diagnostic stream for the entry.

     "forwhat" can be JD_ELEMENT to set information specific to
      the current element, or JD_JOB to set diagnostics pertaining
      to the job in general.

   Returns:  1 on success
             0 on failure (bad job handle or element record)


   -------------------------------------------------------------------
   int ji_get_diagnostics (void *jobhandle, int forwhat, char *fname);
   -------------------------------------------------------------------

   Get the diagnostics for the job or current job element. "fname"
   should point to a buffer at least 128 characters long where the
   diagnostic information should be placed: the buffer will be filled
   in with a filename by this routine. If this routine returns 2,
   it is the calling routine's responsibility to delete the file in
   "fname" when it is finished with the data; if this routine returns
   1, then the calling routine must NOT delete the file in "fname".
   If 0 is returned, then no diagnostic information is available
   for the element.

     "forwhat" can be JD_ELEMENT to retrieve information specific to
      the current element, or JD_JOB to retrieve diagnostics pertaining
      to the job in general.

   Returns: 2 "fname" is valid - delete it when finished
            1 "fname" is valid - do NOT delete it when finished
            0 "fname" is invalid - no diagnostic information available.


   ------------------------------------------------------------------
   void ji_increment_time (unsigned char *tm, unsigned int add_secs);
   ------------------------------------------------------------------

   Add "add_secs" seconds to the date and time specified in "tm",
   which should be in the format described under "get_date_and_time".
   You can subtract seconds from a time by passing a negative value for
   "add_secs" to this function.


   ----------------------------------
   void *ji_get_job_by_id (char *id);
   ----------------------------------

   Given the ID number of a job (the "jobid" element of a JOBSTRUCT
   structure), return a usable handle for that job if it still exists in
   the queue. This function allows you to re-open a job by reference
   without holding on to its jobhandle between calls. If this function is
   successful, the job handle it returns can be used in any other ji_*
   function expecting a job handle. The job handle is not open on return.

   Returns: Non-NULL on success
            NULL on failure (job no longer exists, or is locked).


   ---------------------------------------------------------------------
   int ji_get_job_times (void *jobhandle, char *submitted, char *ready);
   ---------------------------------------------------------------------

   Get the time of original submission for a job and the time it is
   scheduled for processing. "submitted" and "ready" are the submission
   and ready times respectively, in the same format as returned by
   get_date_and_time. You can pass NULL for either of these parameters if
   you do not require the value. The ready time for a job can be
   inspected at any time, but the submission time for a job can only be
   retrieved if the job has been successfully opened using ji_open_job.

   Returns: Non-zero on success
            0 on failure (invalid job handle)

5.3 - Network and user database query functions


   ------------------------------------------------------------------
   int get_first_group_member (char *group, char *host, char *member,
      int mlen, void **data);
   int get_next_group_member (char *member, int mlen, void **data);
   int end_group_scan (void **data);
   ------------------------------------------------------------------

   Routines for enumerating the members of a group. First, call
   "get_first_group_member", passing the name of the group in "group".
   The host and username information will be returned in "host" and
   "member" respectively, with "mlen" controlling the maximum nunber of
   characters written into the member name (allocate at least 128
   characters for both "host" and "member"). If "get_first_group_member"
   returns non-zero, call "get_next_group_member" repeatedly to iterate
   through the group's membership. When you have finished scanning
   through the membership, call "end_group_scan". You should pass the
   same address for "data" to all calls - these functions store state
   information there.

   If "get_first_group_member" returns a non-zero value, then you must
   call "end_group_scan" when you have finished scanning, whether or
   not you actually reach the end of the group's membership.



   --------------------------------------------------------------------
   int is_valid_local_user (char *address, char *username, char *host);
   --------------------------------------------------------------------

   Check whether "username" identifies a valid local user. This function
   differs from "is_local_address" in that it does not resolve aliases
   or synonyms, and will only return a success result if the address
   represents a genuine, existing local user. Note that the "address"
   parameter must have no domain portion - this routine expects a user
   name portion only.

   As with "is_local_address", this routine may modify the "username"
   and "host" parameters - allocate at least 256 characters for each
   of these parameters.

   Returns   1 if the address is a valid local user
             0 if the address does not represent a known user


   ------------------------------------------------------------------
   int is_group_member (char *host, char *username, char *groupname);
   ------------------------------------------------------------------

   Determine whether or not a user is a member of a specified group.

   Returns:  1: the group exists and the user is a member
             0: the group exists and the user is not a member
            -1: the group does not exist


   --------------------------------------------------------------------
   int get_first_user_details (char *host, char *match, char *username,
      int ulen, char *address, int alen, char *fullname, int flen,
      void **data);
   int get_next_user_details (char *username, int ulen, char *address,
      int alen, char *fullname, int flen, void **data);
   int end_user_scan (void **data);
   --------------------------------------------------------------------

   Enumerate the local users on the current system.

      "match" - currently ignored; pass an empty string.
      "username" - receives at most "ulen" bytes of the user's
         login name
      "address" - receives at most "alen" bytes of the user's
         e-mail address.
      "fullname" - receives at most "flen" bytes of the user's
         full (personal) name.

   If "get_first_user_details" returns a non-NULL value, then you must
   call "end_user_scan" when you have finished scanning users, whether
   or not you actually reach the end of the user list.

   Returns:  1 if valid user details were found and returned
             0 on error, or if no further users exist.


   --------------------------------------------------------------------
   int get_user_details (char *host, char *match, char *username,
      int ulen, char *address, int alen, char *fullname, int flen);
   --------------------------------------------------------------------

   Return information about a single specific user, whose username is
   contained in "match". All other parameters are the same as those in
   "get_first_user_details".

   Returns:  1 on success
             0 on failure (no such user)


   ---------------------------------------------------------
   void read_pmprop (char *userid, char *server, PMPROP *p);
   ---------------------------------------------------------

   Get extended features settings for the specified user. "Extended
   features" is a Pegasus Mail term and covers things like automatic
   mail forwarding and address synonyms. Any given user will not
   necessarily have an extended features record.

   The principal field of the "PMPROP" structure from Mercury's
   point of view is the "gw_auto_forward", which contains the address
   to which all mail delivered by Mercury should be forwarded.


   ---------------------------------------------------------------
   int change_ownership (char *fname, char *host, char *newowner);
   ---------------------------------------------------------------

   Change the network ownership of the specified file. This function
   is only meaningful when Mercury is using a network interface
   module that supports the concept of changing the ownership of a
   file.

   Returns:  1 if the file's ownership was successfully changed
             0 on error, or if the function is not supported.


   -----------------------------------------------------------------
   int begin_single_delivery (char *uic, char *server, void **data);
   void end_single_delivery (void **data)
   -----------------------------------------------------------------

   These functions should not be called by Daemons; they are only
   meaningful to true Mercury protocol modules.


   -----------------------------------------------------------
   INT_32 create_object (char *objectname, INT_32 objecttype,
      char *id, INT_32 flags);
   -----------------------------------------------------------

   Create a new object in the system. "objectname" can be any valid
   filename for the system in use, but must not contain spaces, and
   should be 8 characters or less if compatibility with 16-bit systems
   is required. "id" is the object's default identification (or personal
   name) string.

      "objecttype" can be OBJ_USER, or OBJ_ADMINISTRATOR

      "flags" is a bitmap containing user creation control flags. The
         following values are possible:
         1  Copy any default messages into the new user's mailbox

   This routine will fail if a user already exists with the name "username",
   or if the user's mail directory cannot be created.

   This routine is only available in Standalone Mercury systems - it is
   not implemented in Network-level plugins.

   Returns:  1 on success
             0 on failure


   ----------------------------------------------------------------
   int verify_password (char *username, char *host, char *password,
      int select);
   ----------------------------------------------------------------

   Given a username and host as returned by "is_local_user", verify
   a password for that user.

   "password" The password to verify. This may or may not be case-
              sensitive, depending on the underlying operating system.

   "select"   indicates the type of password to verify: this variable
              can have the following values

                 SYSTEM_PASSWORD (1)   The user's login password
                 APOP_SECRET (2)       The user's APOP shared secret

   Returns:  1 if the password appears to be valid
             0 if the password is invalid


   ----------------------------------------------------------------
   INT_32 set_password (char *username, char *host, char *password,
      INT_32 select);
   ----------------------------------------------------------------

   Set the user's System password or APOP secret. "username" and "host"
   should be values returned by a previous call to either
   "is_valid_local_user" or "get_*_user_details". "select" should be
   either APOP_SECRET or SYSTEM_PASSWORD. This function may not be
   available on all systems.

   Returns:  1 on success
             0 on failure



5.4 - Miscellaneous functions


   -----------------------------------------------------------------
   DWORD mercury_command (DWORD selector, DWORD parm1, DWORD parm2);
   -----------------------------------------------------------------

   This function provides an extended interface into Mercury's core
   services. In general, less-frequently-used functions, or functions
   with simple parameter lists may be exposed via this mechanism.

   The function operates much like the Windows "SendMessage" function:
   you indicate which function is required in the "selector" parameter,
   and the meanings of the "parm1" and "parm2" parameters will depend
   on the selector. The return from this function also depends on the
   selector value. Mercury does not return from this function until
   the command has completed.

   At the time of writing, the following selectors and their parameter
   lists are defined; others will be added over time:

      Selector:  GET_MODULE_INTERFACE
      Parm1:     (char *) Pointer to name of module to locate
      Parm2:     Unused, must be 0
      Returns:   Protocol module command interface function pointer
      Comments:  This message is only for use by Mercury protocol
                 modules and should not be used by Daemons.

      Selector:  ADD_ALIAS
      Parm1:     (char *) Pointer to alias to add
      Parm2:     (char *) Pointer to address to associate with alias
      Returns:   Non-zero on success, 0 on failure
      Comments:  Add an alias to the system alias file. This function
                 will fail if an alias already exists using the string
                 supplied. Any type of alias may be added, including
                 Daemon:, File: and TFile: aliases.

      Selector:  DELETE_ALIAS
      Parm1:     (char *) Pointer to alias to delete
      Parm2:     Unused, must be 0
      Returns:   Non-zero on success, 0 on failure
      Comments:  Deletes the specified alias from the system alias file.
                 A Daemon that adds its own aliases for the addresses
                 it services will typically call this function before
                 adding its alias.

      Selector:  RESOLVE_ALIAS
      Parm1:     (char *) Pointer to alias to resolve
      Parm2:     (char *) Pointer to buffer to place address
      Returns:   Non-zero on match, 0 on no match or failure
      Comments:  Attempts to resolve the specified alias to its real-
                 world address form. You must allocate at least 180
                 characters to the buffer (parm2) parameter.

      Selector:  RESOLVE_SYNONYM
      Parm1:     (char *) Pointer to synonym to resolve
      Parm2:     (char *) Pointer to buffer to place address
      Returns:   Non-zero on match, 0 on no match or failure
      Comments:  Attempts to resolve the specified string as a
                 synonym (two-way alias). For more information on
                 synonyms, see the Mercury documentation. You must
                 allocate at least 180 characters for the buffer
                 (parm2) parameter.

      Selector:  QUEUE_STATE
      Parm1:     0 to query current state, 1 to set state
      Parm2:     1 to pause processing, 0 to enable it
      Returns:   The state of queue processing prior to the call
      Comments:  Allows you to pause and unpause the core module's
                 processing of the mail queue.



   ----------------------------------------------------------------
   char *get_date_string (int selector, char *buf, BYTE *datetime);
   ----------------------------------------------------------------

   Return a properly-formatted date string.

     "selector"    - either RFC_821_TIME or RFC_822_TIME
     "buf"         - where the formatted string should be written
     "datetime"    - NULL for the current time, or a 7-byte time structure

   RFC821 time format uses a 2-digit year and is only used in timestamp
   headings in "Received" headers for messages.

   RFC822 time is conventional RFC822/RFC1123 date format, using a
     4-digit year.

   Both formats include a time zone if one is defined on the system.

   "buf" must be at least 40 characters in length.

   See "get_date_and_time" for a description of the format of "datetime"

   Returns:  buf


   ---------------------------------
   char *rfc822_time (char *buffer);
   char *rfc821_time (char *buffer);
   ---------------------------------

   Provided for backwards-compatibility only. Use "get_date_string"
   instead of these functions.


   ------------------------------------------------------
   INT_32 select_printer (char *device_name, int maxlen);
   ------------------------------------------------------

   Bring up a dialog that prompts the user to select a printer from
   those available on the current system. Both local and network
   printers are listed. The printer name is returned in "device_name"
   if the user clicks "OK". The name returned is suitable for use in
   the "print_file" function (see below).

   Returns:         1 if OK was clicked
                    0 if Cancel was clicked


   ---------------------------------------------------------------------
   INT_32 (* PRINT_FILE) (char *fname, char *printername, UINT_32 flags,
      INT_32 lrmargin, INT_32 tbmargin, char *title, char *username,
      char *fontname, INT_32 fontsize);
   ---------------------------------------------------------------------

   Print a file or mail message. This function provides a simple way of
   printing textual data, and has useful formatting options designed to
   handle normal textual mail messages. Only textual data can be printed
   - this routine will not print HTML, RTF or other formatted data types.

      "fname" is the full pathname of the file to print

      "printername" is the name of the printer on which the file is to be
         printed; this can be any local or network printer. You can use the
         "select_printer" function (see above) to allow the user to choose
         a printer. If you pass NULL for this parameter, Mercury will use
         the system's default printer.

      "flags" is a bitmap of flags controlling the way the file will be
         printed; the following values are available:

            PRT_MESSAGE    Print as an RFC822 mail message
            PRT_REFORMAT   Reformat long lines when printing
            PRT_TIDY       Print only "important" headers
            PRT_FOOTER     Print a footer on each page
            PRT_NOHEADERS  Print no message headers
            PRT_FIRSTONLY  Print only first line of headers
            PRT_ITALICS    Print quoted text in italics

      "lrmargin" is the margin in millmetres (mm, 25.4mm == 1 inch) to
         allow at the left and right sides of the printed page.

      "tbmargin" is the margin in millimetres to allow at the top and
         bottom of the printed page.

      "title" is an optional string Mercury will use to identify the
         document in the Windows print manager queue; the title is not
         itself printed anywhere on the document. This paramter can be
         NULL if not required.

      "username" is the username Mercury should print in the footer on
         each page if that option is selected. This parameter can be NULL
         if it is not required.

      "fontname" is the name of the font to use when printing. If this
         parameter is NULL or zero-length, the default font and size will
         be used.

      "fontsize" is the size in points of the printer font. This
         parameter is ignored if "fontname" is NULL or zero-length.
   
   Returns:         1 on successful printing
                    0 on printing error.



5.5 - File I/O and parsing routines

The functions in this section allow Daemons to perform message and MIME
parsing on mail messages. The first group of functions allow the
manipulation of files in a portable manner (since open file handles
cannot be passed between DLLs and programs), while the second group of
functions perform parsing and analysis of complex mail messages.


   ------------------------------------------------
   INT_32 fm_open_file (char *path, UINT_32 flags);
   ------------------------------------------------

   Open any file. The file is opened in binary mode, so line endings
   are returned as CRLF, not as simply LFs. The handle returned by this
   function can be passed to any of the parsing or file manipulation
   functions that expect an internal file handle.

   "flags" can have any of the following values:

      FF_NO_LINE_ENDINGS - tells "fm_gets" to remove CRLF terminators
        from each line it reads from the file

   Returns:  > 0 (the file reference) on success
             0 on failure


   -----------------------------------------------------
   INT_32 fm_open_message (IMESSAGE *im, UINT_32 flags);
   -----------------------------------------------------

   Provided for backwards compatibility with Pegasus Mail. Daemons should
   use "fm_open_file" instead of calling this function.


   ---------------------------------
   int fm_close_message (INT_32 id);
   ---------------------------------

   Close a file opened using "fm_open_file" or "fm_open_message".

   Returns:  1 on success
             0 on failure


   --------------------------------------------------
   char (*fm_gets (char *buf, INT_32 max, INT_32 id);
   --------------------------------------------------

   Read the next line from a file opened using "fm_open_file" or
   "fm_open_message". At most "max - 1" characters are read from the
   file, and the line is always nul-terminated. CRLF characters will
   be present unless the file was opened using the "FF_NO_LINE_ENDINGS"
   flag, or possibly for the last line of the file.

   Returns:  "buf" on success
             NULL on failure or EOF.


   ---------------------------
   INT_16 fm_getc (INT_32 id);
   ---------------------------

   Read the next character from the file.

   Returns:   the character on success
              EOF on failure


   -------------------------------------
   void fm_ungetc (INT_16 c, INT_32 id);
   -------------------------------------

   "unget" the last character retrieved using "fm_gets" or "fm_getc".
   Exactly one level of character may be "ungot" using this function.


   ---------------------------------------------------------
   INT_32 fm_read (INT_32 id, char *buffer, INT_32 bufsize);
   ---------------------------------------------------------

   Read at most "bufsize" bytes from the file. No character conversions
   are done, and the data may not necessarily end on a line boundary.

   Returns:  > 0 on success (the number of bytes read)
             = 0 on EOF
             < 0 on error


   ------------------------------
   INT_32 fm_getpos (INT_32 fil);
   ------------------------------

   Get the current offset in the open file. The return from this function
   may be passed to "fm_setpos" to reposition the file at a given point.

   Returns:  Current file offset.


   ---------------------------------------------
   INT_16 fm_setpos (INT_32 fil, INT_32 offset);
   ---------------------------------------------

   Reposition the file pointer for an open file. "offset" must be a
   value returned by "fm_getpos", or 0 to rewind the file. You should
   not assume a linear relationship between "offset" and the data in the
   file.

   Returns:  > 0 on success
             0 on error.


   --------------------------------------------------------------
   INT_32 fm_get_folded_line (INT_32 fil, char *line, int limit);
   --------------------------------------------------------------

   Read a header from a message, "unfolding" continuation lines as
   required. This function understands RFC822 line folding rules for
   message headers, and will return a single line containing the full
   extent of a header. This is an extremely useful function for reading
   through the headers of a message without worrying about dealing with
   continuation lines. This routine guarantees that the returned line
   will be nul-terminated and will not end in CRLF.

   There is usually no reason to use this function to read the body of
   a mail message, and doing so may lead to unexpected results.

   Example: if the next lines in the message are:

       From: David Harris
         <david@pmail.gen.nz>
         (Pegasus Mail Author)

   Then this function will return the following single string:

       From: David Harris <david@pmail.gen.nz> (Pegasus Mail Author)

   Returns:  > 0 on success (number of characters in line)
             0 if the end of headers has been reached.


   -------------------------------------------------------------------
   char (*fm_find_header (INT_32 fil, char *name, char *buf, int len);
   -------------------------------------------------------------------

   Locate the header whose keyword is passed in "name" in the specified
   message file. The entire line without the keyword is returned, with any
   continuations unfolded into the data.

   Returns:  "buf" on success (points to start of field body)
             NULL on failure.


   -----------------------------------------------------------
   int fm_extract_message (void *job, char *fname, int flags);
   -----------------------------------------------------------

   Take an open Mercury mail job and extract its contents to the file
   specified in "fname".

   "flags" can have combinations of the following values:

      FFX_APPEND       - append to the file if it exists - don't overwrite
      FFX_NO_HEADERS   - omit the message headers when writing to the file
      FFX_TIDY_HEADERS - write only "significant" headers to the file
      FFX_NOT_OPEN     - the job is not currently open

   If the "FFX_NOT_OPEN" flag is specified, then this routine will open
   and close the job. When passing the "job" parameter supplied to the
   "daemon" function, you must not specify this flag - the job is already
   open and must remain open during processing.


   -------------------------------------------
   int parse_header (INT_32 fil, IMESSAGE *m);
   -------------------------------------------

   Perform comprehensive parsing on the open message file "fil", which
   should have been opened using "fm_open_file". The information gleaned
   from the message's headers is stored in the IMESSAGE structure, "m".
   Applications using this function will be particularly interested in
   the "flags" field of this structure, since it contains extensive
   information about attachments and MIME formatting that might be
   present in the message.

   Returns:  1 on success
             0 on failure (very rare)


   ---------------------------------------------------------------
   int mime_prep_message (INT_32 fil, char *fname, int headers);
   ---------------------------------------------------------------

   Given a non-multipart MIME message, create a "sanitized" version of
   the message data in the file named by "fname". MIME encodings are
   decoded as required, and any character set conversions that might
   be needed are performed. If you pass an empty string for "fname",
   Mercury will create a temporary file for you, returning the name
   of that file in "fname".

   Note that you must not call this function for a multipart message:
   to extract a sanitized section from a multipart message, call
   "parse_mime", traverse the multipart list until you find the section
   you need, then pass that section to "fake_imessage".

   If "headers" is non-zero, then the message headers from the message
   will be included in the output file, otherwise they will be omitted.

   Returns:  > 0 on success
             0 on failure


   ----------------------------------------
   int parse_mime (INT_32 fil, IMIME *m);
   ----------------------------------------

   Given any valid MIME message, parse its contents and return them in
   the "IMIME" structure, "m".

   The IMIME structure is a relatively complicated variable record which
   can represent any MIME format, including arbitrarily nested Multipart
   MIME messages. The fields in an IMIME structure are as follows:

      primary     - the logical message type, e.g MP_TEXT
      secondary   - the logical message sub-type, e.g. MPT_PLAIN
      encoding    - the encoding for this part, e.g ME_BASE_64
      disposition - either MD_ATTACHMENT or MD_INLINE
      p_string    - the actual primary type string, e.g "TEXT"
      s_string    - the actual secondary sub-type string, e.g "PLAIN"
      description - "Content-description" for this part, if any
      section     - the number of this section within the message
      fname       - the filename associated with this part, if any

      d           - a union, one data element from which will contain
                    part-specific information about this part. You
                    should select the appropriate record based on the
                    value of "primary".
                       primary == MP_TEXT        : use "mpt"
                       primary == MP_MULTIPART   : use "mpp"
                       primary == MP_APPLICATION : use "mpa"
                       primary == MP_MESSAGE     : use "mpm"

   The "mpm" record within the structure contains a list of other parts
   in the message. It should be traversed by dereferencing the "top"
   field of the "partlist" entry and casting its data field to "IMIME *".

      Example: code to traverse the parts in a multipart message

      IMIME *m, *m2;
      LNODE *cur;

      if (m->primary == MP_MULTIPART)
         {
         for (cur = m->d.mpp.partlist.top; cur != NULL; cur = cur->next)
            {
            m2 = (IMIME *) (cur->data);
            //  do whatever you need with that section here.
            }
         }

   Note that it is valid, or even normal for a sub-part of a multipart
   message to be itself a Multipart item. When this happens, the parts
   of the nested item will have been properly parsed into its own "mpp"
   structure, and can be followed by traversing its partlist using the
   same technique.

   Returns:  1 on success
             0 on failure (rare, indicates serious malformatting)


   --------------------------
   void free_mime (IMIME *m);
   --------------------------

   Call this function when you have finished with an IMIME structure
   successfully parsed by "parse_mime". This function deallocates any
   lists or other memory allocation created to store the structure of
   the MIME message.


   -----------------------------------------------------------------
   int fake_imessage (IMESSAGE *im, char *dest, char *src, IMIME *m,
      char *boundary);
   -----------------------------------------------------------------

   "Sanitize" a single section of a multipart message. This function
   extracts the section of the message contained in "src" and identified
   by "m" (which is presumed to be a part from the "partlist" list of a
   multipart message) to the file named in "dest". Any necessary decoding
   and conversion is done on the section. "Boundary" should point to the
   boundary string that delimits the section in the file (you should
   usually pass "d.mpp.boundary" from the enclosing message's IMIME
   structure for this).

   Parsing information about the extracted section is stored in the
   IMESSAGE structure "im".

   It is the responsibility of the calling function to delete the file
   "dest" when it is no longer required.

   Returns:  1 on success
             0 on failure


   -----------------------------------------------
   int decode_mime_header (char *dest, char *src);
   -----------------------------------------------

   Search for and decode 8-bit data encapsulated according to RFC-1522
   rules in a string of data (presumably a header extracted from a message
   using fm_get_folded_line()). No line breaks are permitted in the input
   data.

     "dest" should point to a buffer where the decoded header string should
        be written. The destination buffer should be at least as large as
        the input buffer.

     "src" should point to the candidate string, which may contain multiple
        RFC-1522-encoded strings.

   Returns:  1 if encoded data was found (dest is valid)
             0 if no encoded data was found (dest is invalid)
            -1 on error (bad format or unrecognized char set)


   --------------------------------------------------------
   int encode_mime_header (char *dest, char *src, int raw);
   --------------------------------------------------------

   Encode a string using RFC1522 rules; transformation only occurs if the
   input string contains 8-bit data.

     "dest" should point to a buffer where the encoded string should be
        written. The destination buffer should be at least twice as large
        as the input buffer. Note that this routine NEVER generates
        multiple encoded blocks in the string - either the whole string is
        encoded, or none of it is. Furthermore, this routine only ever
        generates the quoted-printable RFC1522 variant, never the BASE64
        encoding, which seems to me to be a ludicrous thing to place in the
        headers of a mail message.

     "src"  should point to the source string, which may or may not contain
        8-bit data. "src" is expected to be in the WinANSI character set.

   Returns:  1 if data was encoded (dest is valid)
             0 if no data was encoded (dest is invalid)


   ----------------------------------------------------------
   int encode_base64_str (char *dest, char *src, int srclen);
   ----------------------------------------------------------

   Encode an arbitrary string of data in BASE64 format. Note that this
   encoding process will produce an output line approximately a third
   larger again than the input, and that MIME message conventions on
   line length are ignored - the output will be a single line. This
   function is useful for protocols like RFC2554 that require string
   data to be transmitted as a BASE64-encoded quantity.

      "dest" should point to a buffer where the BASE64 encoded data
         should be written. The destination buffer should be at least twice
         as large as the input buffer.

      "src" is the source string, which may contain any data (it is not
         limited to textual or ASCII characters).

      "srclen" is the length in bytes of "src".

   Returns   1 on success
             0 on failure (very, very rare).


   -----------------------------------------------------------
   int decode_base64_str (char *dest, char *src, char *table);
   -----------------------------------------------------------

   Decode a string encoded using BASE64.

      "dest" should point to a location where the decoded data is to
         be written. This buffer should be at least the same size as
         the input buffer. The decoded data may be binary in nature -
         there is no guarantee that it will be a nul-terminated C
         string.

      "src" should point to the string to decode

      "table" can point to a 128-byte character table used to convert
         high-bit characters in the decoded data to an alternative
         character set. This value will typically be NULL.

   Returns:  1 on success
             0 on failure (malformatted BASE64 data)



5.6 - Message composition routines

Mercury provides an easy way to create relatively complex mail messages
in MIME formats. You can create simple messages, messages with
attachments, multipart/alternative messages containing different versions
of the same data, and MIME digests containing other mail messages.


   -------------------------------------------------------
   void *om_create_message (UINT_32 mtype, UINT_32 flags);
   -------------------------------------------------------

   Create a message. The message type is set to "mtype" and the message
   flags are set to "flags". The handle returned by this function must be
   passed to all subsequent message building functions. Messages created
   by Mercury are always valid MIME messages - non-MIME message types can
   not be created (nor is it any longer desirable to do so).

   "mtype" must be one of the following values:

      OM_MT_PLAIN         - A simple, single-part text/plain message
      OM_MT_MULTIPART     - A multipart/mixed message
      OM_MT_ALTERNATIVE   - A multipart/alternative message
      OM_MT_DIGEST        - A multipart/digest message

   "flags" is a bitmap composed of the following possible values:

      OM_M_8BIT           - The message body contains 8-bit data

   The "flags" field can be set later using the "om_add_field" function
   and the OM_MF_FLAGS selector.

   Returns:    Non-NULL on success (message handle)
               NULL on failure


   ------------------------------------------
   INT_32 om_dispose_message (void *mhandle);
   ------------------------------------------

   Release the storage used by a message. It is an error to use the
   "mhandle" parameter after it has been passed to this routine. This
   function must be called when the message is no longer needed, to
   deallocate internal buffers.

   It is the calling routine's responsibility to deal with the body file
   and any files attached to the message.

   Returns:    > 0 on success
               = 0 on failure (invalid handle)


   ------------------------------------------------------------------
   INT_32 om_add_field (void *mhandle, UINT_32 selector, char *data);
   ------------------------------------------------------------------

   Add a field to a message.

   "selector" can be any one of the following values:

      OM_MF_TO       - set the master recipient of the message
      OM_MF_SUBJECT  - set the subject field for the message
      OM_MF_CC       - set the secondary recipients of the message
      OM_MF_FROM     - set the originator of the message.
      OM_MF_BODY     - set the filename containing the message body
      OM_MF_RAW      - add a raw header for the message.
      OM_MF_FLAGS    - set the message's global flags

   If the "OM_MF_FROM" field is not set for a message, it will default
   to the postmaster address for the system. It is an error to attempt
   to send a message for which "OM_MF_TO" has not been set; all other
   fields are optional.

   The "OM_MF_RAW" selector allows the calling process to add any header
   it wishes to the message. The header should be passed complete,
   including the header keyword, but without a terminating CRLF pair.
   Mercury guarantees that headers added to a message will be written to
   the message in the order in which they are added, so you can add
   headers with continuations if you wish. You cannot add any of the
   following headers using "OM_MF_RAW": "To", "Cc", "Subject", "BCC",
   "Content-type", "Content-transfer-encoding", "MIME-Version", or
   "Content-disposition". You can reset all custom headers added using
   OM_MF_RAW by using OM_MF_RAW as a selector and passing NULL for the
   "data" parameter.

   It is explicitly legal to add a field you have already added to a
   message. This allows you to create a message then send it to multiple
   recipients in separate jobs, for example.

   Returns:    > 0 on success
               = 0 on error


   -----------------------------------------------------
   INT_32 om_add_attachment (void *mhandle, char *fname,
      char *ftype, char *description, UINT_32 encoding,
      UINT_32 flags, void *reserved);
   -----------------------------------------------------

   Add an attachment or part to a message.

      "fname"          - the fully-qualified path to the file to attach
      "ftype"          - a content-type string - see below
      "description"    - optional free text description string
      "encoding"       - encoding method - see below
      "flags"          - special settings for the attachment - see below
      "reserved"       - must be 0

   The "ftype" string is a free text atom and can have any value, provided
   it does not contain spaces, tabs or commas.

   "encoding" can have the following values

      OM_AE_DEFAULT  - default encoding (MIME BASE64 encoding)
      OM_AE_TEXT     - simple textual data, unencoded
      OM_AE_UUENCODE - uuencoding
      OM_AE_BINHEX   - Macintosh Binhex format (data fork only)

   "flags" can have the following values

      OM_AF_INLINE   - write the file as a simple textual section
      OM_AF_MESSAGE  - write the message as a Message/RFC822 part

   The file need not exist at the time it is attached.

   Mercury/32 guarantees that attachments will appear in the message in
   the order in which they were attached, so you can create messages
   that depend on the sequence of attachments with confidence.

   Returns:    > 0 on success
               = 0 on failure.


   -----------------------------------------------------
   INT_32 om_write_message (void *mhandle, char *fname);
   -----------------------------------------------------

   Given a message handle that has been populated with fields, body and
   attachments, create a simple disk file containing the final form of
   the mail message.

   Note that this function does *not* create a job, nor does it send
   the message. It simply renders the message structures into their
   final form.

   This routine is called internally by "om_send_message" to create the
   final form of the Mercury queue job. It is guaranteed that calling
   this function then calling "om_send_message" will generate a file
   and a job that are identical (so, this routine can be used to create
   archive copies of outgoing messages).

   Returns:        > 0 on success
                   = 0 on failure


   ------------------------------------------------------
   void *om_send_message (void *mhandle, char *envelope);
   ------------------------------------------------------

   Given a message handle that has been populated with fields, body
   and attachments, create a job in the Mercury queue containing the
   final form of the mail message.

   "envelope" is an optional envelope ("Return-path") address for the
   created message. If NULL or 0-length, the envelope will default to
   the From field, or the postmaster address.

   The return from this job is an open Mercury job handle. It is the
   responsibility of the calling process to make any last-minute job
   settings, then to close or discard the job.

   Returns:    Open Mercury job handle on success
               0 on failure


Notes on composing messages:

1: To create a MIME digest, you must ensure that all the files you attach
to your message are validly-formatted RFC822 mail messages, and that each
has the OM_AF_MESSAGE bit set in its "flags" field. The message body is
ignored for MIME digests, so you may wish to consider creating a "dummy"
first message containing a summary, subscription information or other
information useful to the recipient.

2: The message body file and any attachments must be closed when either
"om_write_message" or "om_send_message" is called.

3: If the "OM_M_8BIT" flag is set for the message, the body of the
message will be sent using the MIME "quoted-printable" encoding scheme.
At present, only the ISO-8859-1 character set (which is the same as the
default MS-Windows character set) is supported for 8-bit data. It is
safe to set the OM_M_8BIT flag for a message that contains only 7-bit
data, but it is an error not to set it for a message that *does* contain
8-bit character data.

4: Address fields ("To:" and "Cc:") can be up to 32000 bytes in length,
and may contain multiple addresses separated by commas. Mercury/32 will
wrap the address fields according to RFC822 line folding rules. Mercury
will *not* expand partial addresses to fully-qualified domain name forms.
It is up to your Daemon to ensure that all addresses are legal and
fully-qualified.



5.7 - Statistics and logging interface

Mercury incorporates a comprehensive Statistics Manager that can keep
running statistics and information about the throughput, performance and
operation of the system. The entire Statistics Manager interface is
available to Daemons and protocol modules. Mercury also provides a System
Messages window into which console-type messages can be written at
various priority levels.


   ----------------------------------------------
   INT_32 st_register_module (char *module_name);
   ----------------------------------------------

   Register a module with the statistics interface. "Module name" is the
   name that should be presented in the statistics manager list for this
   item. This function creates a top-level statistics category in the
   Statistics Manager's list.

   Returns:    Module handle > 0 on success
               0 on failure (very rare)


   ---------------------------------------------
   INT_32 st_unregister_module (INT_32 mhandle);
   ---------------------------------------------

   "Unregister" a module and remove its entries from the statistics list.

   Returns     1 on success
               0 on failure (not registered)


   ----------------------------------------------------------
   INT_32 st_create_category (INT_32 mhandle, char *cname,
      INT_32 ctag, INT_32 ctype, INT_32 dlen, UINT_32 flags);
   ----------------------------------------------------------

   Create a single statistical category within a specified module.

      "mhandle"  module's registered handle

      "cname"    display name for this category

      "ctag"     module's reference handle for this item; this is an
                 arbitrary value supplied by the module that identifies
                 this category, and it must be unique within the items
                 created within a single module.

      "ctype"    type of data represented
                 - STC_INTEGER for integral data
                 - STC_STRING  for string data
                 - STC_DATE    for time/date data

      "dlen"     maximum size of data (for strings)

      "flags"    attributes of this category
                 - STF_CUMULATIVE   data accumulates
                 - STF_PEAK         record only the highest value
                 - STF_UNIQUE       allow only one named instance

   Returns:    > 0 (category handle) on success
                 0 on failure


   ---------------------------------------------------------
   INT_32 st_remove_category (INT_32 mhandle, UINT_32 ctag);
   ---------------------------------------------------------

   Remove a statistical category and delete it from any lists currently
   displayed. "ctag" should be the category tag handle passed when the
   category was created.

   Returns      1 on success
                0 on failure (category not found)


   -------------------------------------------------------
   INT_32 st_set_hcategory (INT_32 chandle, UINT_32 data);
   -------------------------------------------------------

   Set the data for a category given its category handle only. Note that
   a "category handle" is the return from st_create_category, not the
   ctag value passed to that function. The system guarantees that
   category handles will always be globally unique, and may change from
   session to session.

   "data" must be of a type appropriate for the registered category. If
   the category has the "STF_CUMULATIVE" attribute set, then the data are
   accumulated to any data already existing for the category.

   Returns:     > 0 on success
                  0 on failure


   -------------------------------------------------------------------
   INT_32 st_set_category (INT_32 mhandle, INT_32 ctag, UINT_32 data);
   -------------------------------------------------------------------

   Set the data for a category given its module handle and the module's
   internal category handle. "data" must be of a type appropriate for the
   registered category. If the category has the "STF_CUMULATIVE" attribute
   set, then the data are accumulated to any data that already exist for
   the category.

   Returns:     > 0 on success
                  0 on failure


   ----------------------------------------------------------
   INT_32 st_get_next_module (INT_32 mhandle, char *modname);
   ----------------------------------------------------------

   Return the next module in the statistics list. To enumerate the modules
   in the list, set "mhandle" to -1 on the first call, then pass the return
   value in subsequent calls until -1 is returned.

   The module's name is copied into "modname", which should be at least 128
   characters in length.

   Returns:     > 0 on success
                 -1 on no more modules.


   -------------------------------------------------------------
   INT_32 st_get_next_category (INT_32 mhandle, INT_32 chandle,
      char *cname, INT_32 *ctype, INT_32 *clen, INT_32 *cflags);
   -------------------------------------------------------------

   Return the next category belonging to the module "mhandle" in the
   statistics list. To enumerate all the categories in the category list,
   pass the module handle (returned by "st_get_next_module", set "chandle"
   to -1 on the first call then pass the return value in subsequent calls.
   Repeat until the function returns -1.

      "cname" receives the category's descriptive text; allocate
         at least 128 characters

      "ctype" receives the type of the category's data; possible
         values are STC_INTEGER, STC_STRING or STC_DATE

      "clen" receives the maximum length of the data type.

      "cflags" receives the flags associated with the data item;
         possible bit values are STF_CUMULATIVE, STF_PEAK and
         STF_UNIQUE.

   Returns:     > 0 on success
                 -1 on no more categories


   ---------------------------------------------------------
   INT_32 st_get_category_data (INT_32 chandle, void *data);
   ---------------------------------------------------------

   Copy the current data value for the specified category into the
   location pointed to by "data". It is the calling module's
   responsibility to ensure that sufficient space is allocated. For
   STC_INTEGER types, you should allocate 4 bytes; for STC_DATE types,
   you should allocate 4 bytes (the returned value is a C "time_t"
   type); for STC_STRING types, you should allocate the "clen" returned
   by a call to st_get_next_category (which will always be correct for
   integer and date types as well).

   Returns:       1 on succes
                  0 on failure


   --------------------------------------------------------------------
   INT_32 st_export_stats (INT_32 mhandle, char *fname, UINT_32 flags);
   --------------------------------------------------------------------

   Export a single module's statistics, or all modules' stats
   to the file named in "fname" in plain text format.

      "mhandle"  The module handle for which statistics should
                 be exported. If 0, export all modules.

      "fname"    Filename to receive data

      "flags"    If (flags & 1), append data to the file
                 If (flags & 2), omit 0 or undefined categories

   Returns       1 on success
                 0 on failure



   ----------------------------------------------------------
   void logstring (INT_16 ltype, INT_16 priority, char *str);
   ----------------------------------------------------------

   Log a message in the System Messages window

      "ltype"    An arbitrary integer representing the "type" of
                 the message. It is a convention that "major" types
                 should be divisible by 10; other types of data
                 (minor types) will be indented.

      "priority" The level of importance of the message. The user
                 can set the level they want displayed, and messages
                 with a lower priority than that will be discarded.
                 Possible values for this field are:
                 - LOG_DEBUG
                 - LOG_INFO
                 - LOG_NORMAL
                 - LOG_SIGNIFICANT
                 - LOG_URGENT
                 - LOG_NONE

   Developers are strongly urged to use priority values correctly and
   responbsibly, for the benefit of users.


   -------------------------------------------------------------
   void logdata (INT_16 ltype, INT_16 priority, char *fmt, ...);
   -------------------------------------------------------------

   The same as "logstring", but supporting "sprintf"-type formatting.
   See the documentation for "sprintf" in your compiler manual for
   information on valid formatting codes.

   Note that because of the way macros are handled in the C language,
   this function cannot be accessed using the convenience macros in
   "daemon.h" - you must access the function directly from the protocol
   block structure.



Appendix A - Technical issues

A.1 - Function names

When Mercury loads a Daemon, it attempts to locate the functions exported
by the Daemon using the function names. Different compilers may export
the names of your functions in different ways - even though your function
may be called "startup", the compiler may actually export it as
"_startup", for historical reasons.

Mercury always attempts to locate your functions in two ways: the first
attempt prepends a "_" to the function name, while the second searches
for the function name with no leading "_", all in uppercase. The first of
these methods will reliably detect functions exported by Borland
compilers, while the second mechanism will usually detect functions
exported by the Microsoft Visual C++ compiler family.

If you find that your functions do not seem to be getting called by
Mercury, check on the format of the name exported by your compiler - this
is the most likely source of the problem.

