From:	CSBVAX::MRGATE!RELAY-INFO-VAX@CRVAX.SRI.COM@SMTP 26-AUG-1988 08:55
To:	ARISIA::EVERHART
Subj:	VMSMAIL_PROFILE.DATA  mail file format


Received: From KL.SRI.COM by CRVAX.SRI.COM with TCP; Wed, 24 AUG 88 09:53:48 PDT
Received: from jade.berkeley.edu by KL.SRI.COM with TCP; Wed, 24 Aug 88 00:11:48 PDT
Received: by jade.berkeley.edu (5.54 (CFC 4.22.3)/1.16.18B)
	id AA16584; Tue, 23 Aug 88 23:37:12 PDT
Message-Id: <8808240637.AA16584@jade.berkeley.edu>
Date: Tue, 23 Aug 88 23:37 PDT
From: CARL FUSSELL <CARL%SCU.BITNET@jade.berkeley.edu>
Subject: VMSMAIL_PROFILE.DATA  mail file format
To: INFO-VAX@kl.sri.com

We just installed VMS 5.0 this past weekend.  We are using a couple
programs that depended upon the format of the old VMSMAIL.DAT file.  To
get them to run, we had to figure out (at least in part) the format of
the replacement V5.0 file  VMSMAIL_PROFILE.DATA.   Since questions have
been asked here in the past about the format, I thought I would post
what we learned as well as a test program (the test program is a hacked
up version of John Lavagnino's "unread" program).

We don't have the VMS fiche so what we came up with is incomplete.  If
anyone fills in more of the unknowns, we would really like to hear.

Thanx and hope this saves others a little time...

Carl Fussell
Santa Clara Univ
CARL@SCU.BITNET
================================================================
                   VMS VMSMAIL_PROFILE File Format

This was all determined empirically and as such, is not comprehensive or
even guaranteed to be accurate.  A record containing 1 user's information
seems to be broken down as follows:

    Field 1:    USERNAME (31 characters)  (Key)

    Field 2:    FIELDTYPE    (16 bits - Numeric)
                FIELDLENGTH  (16 bits - Numeric, gives length in # of bytes)
                FIELD        (Numeric or character field)

    Field 3-n:  {same as 2, just repeats)

Maximum record length containing fields 1-n is:  2048 bytes



The field types appear to be as follows:

  TYPE  DESCRIPTION         LENGTH              DATA TYPE IN FIELD
    1   Newmail count       length: 2 bytes     Numeric field
    2   Flags               length: 2 bytes     Numeric field (bits)
    3   Mail Directory      length: n chars     Variable len char field
    4   Forwarding Addr     length: n chars     Variable len char field
    5   Personal Name       length: n chars     Variable len char field
    6   Unknown
    7   Unknown
    8   Unknown
    9   Default LP Queue    length: n chars     Variable len char field
   10   Unknown
   11   Unknown
   12   Unknown
   13   Default LP Form     length: n chars     Variable len char field

The flag field appears to be a bit field defined as follows:

  BIT   WEIGHT  DESCRIPTION
    0      1    Copy Self on SEND
    1      2    Copy Self on REPLY
    2      4    Auto Purge Disable
    3      8    Copy Self on FORWARD
    4     16    CC: Prompt Enable

Field types 1 and 2 (newmail count and flags) always appear to be the
first two following the key field (Username).  The remaining types can
appear in any order so you must look at the type field to determine
just what type of field you are looking at.  This allows DEC to add
as many new capabilities as they need and allow a variable amount
of room for each capability.
Sample records:  (field values all in decimal)

   User JSMITH with 5 new mail messages and his mail directory
        being [JSMITH.MAIL] and Autopurge disabled.

      JSMITH  -25 more spaces- 1  2  5  2  2  4  3  7 [.MAIL]

   User PWILLIAMSON with 71 new mail messages, Personal Name =
        Peter W., Mail dir = [PWILLIAMSON.MAILBOX] , Copy Self
        on Reply + Auto purge + CC prompting enabled.

      PWILLIAMSON -20 more spaces- 1  2  71  2  2  18  5  8  Peter W.
      3  10 [.MAILBOX]

   Again, the numeric fields are 16 bits.  Looking at say JSMITH byte
   by byte you would see:

      JSMITH  -25 more spaces- 1 0 2 0 5 0 2 0 2 0 4 0 3 0 7 0[.MAIL]

===========================================================================
                       Sample Program  (Vax C)
===========================================================================
    Includes  V5PROFILE.H + VMSMAIL.C + VMSMAIL.CLD + VMAKE.COM
=====================  V5PROFILE.H  =======================================
/* v5profile.h */
/* Hacked out of the VMS (V4.x) UNREAD program that I believe was done by
   John Lavagnino, Brandeis University.
*/

#define V5MAX_VMSMAIL_RECORD    2048
#define V5VMSMAIL_KEY_SIZE        31
#define VMSMAIL_NAME            "VMSMAIL_PROFILE"
#define VMSMAIL_DEFAULT         "SYS$SYSTEM:.DATA"

/* Format of 5.0 mail profile file is:
        31 chars = Username
        Remainder = {Type} {Length} {Data} repeating... */

/* Type field known definitions */
#define VMSMAIL_NEWMAIL         1           /* new mail count */
#define VMSMAIL_FLAGS           2           /* flag field-see bit defs below */
#define VMSMAIL_MAILDIR         3           /* name of user's mail subdir */
#define VMSMAIL_FORWARDADDR     4           /* mail forwarding address */
#define VMSMAIL_PERSONALNAME    5           /* user's personal name */
#define VMSMAIL_QUEUE           9           /* default queue for printing */
#define VMSMAIL_FORM            13          /* default print form to use */

/* Flag field bit definitions */
#define VMSMAIL_CS_SEND         1           /* Msg copy to self on SEND */
#define VMSMAIL_CS_REPLY        2           /* Msg copy to self on REPLY */
#define VMSMAIL_NOAUTOPURGE     4           /* Do not autopurge WASTEBASKET */
#define VMSMAIL_CS_FORWARD      8           /* Msg copy to self on FORWARD */
#define VMSMAIL_CC_PROMPT       16          /* Prompt for carbon copies- CC: */

struct v5vmsmail_record {
        char vmsmail_username[V5VMSMAIL_KEY_SIZE];
        unsigned short mailcnt_type;
        unsigned short mailcnt_len;
        unsigned short mailcnt;         /* count of unread messages */
        unsigned short flags_type;
        unsigned short flags_len;
        unsigned short flags;           /* user mail account flags */
        char field[V5MAX_VMSMAIL_RECORD-V5VMSMAIL_KEY_SIZE-12];
};
========================  VMSMAIL.C  ======================================
/* vmsmail.c */
#module         VMSMAIL          "V1.0"

#include        climsgdef
#include        descrip
#include        jpidef
#include        psldef
#include        rms
#include        ssdef

#include        "uafdef.h"
#include        "uaidef.h"

#include        "v5profile.h"

#define TRUE    1
#define FALSE   0

/*** Various username strings and their descriptors ***/

/*  "firstname" is the original username off the command line that
  we're after.  (Here and elsewhere, 1 is added to the length to avoid
  problems with those trailing nulls getting stuck on; for the most
  part, ordinary null-terminated strings are not being used in this code.)
    "username" is the current username that we're working on, possibly
  different from the original because of forwarding.  (To escape from
  a possible forwarding loop, we allow only MAX_FORWARDINGS forwarding
  steps before we give up on it.)
    The real lengths of these two names, with trailing blanks removed,
  are in firstname_len and username_len.
    The username of the process running the program is "ourname".
    "username_entity" specifies what we called the command-line username
  parameter in the .CLD for this program. */

static char username_buf[UAF$S_USERNAME + 1];
static char firstname_buf[UAF$S_USERNAME + 1];
static char ourname_buf[UAF$S_USERNAME + 1];

/* (tws) Save contents of firstname_buf so we can print it out rather
   than the translated value if there is no such user */
static char savefirstname[UAF$S_USERNAME + 1];

/* (tws) Save number of newmail messages so we can print out count of
   unread messages.... */
static int savenewmail=0;

static short firstname_len, username_len;

static readonly struct dsc$descriptor_s username_desc = {
        UAF$S_USERNAME, 0, 0, &username_buf
};

static readonly struct dsc$descriptor_s firstname_desc = {
        UAF$S_USERNAME, 0, 0, &firstname_buf
};

#define MAX_FORWARDINGS         10
static int forwardings;

static readonly $DESCRIPTOR(username_entity, "USERNAME");

/*** The VMSMAIL_PROFILE.DATA file. ***/

static struct FAB vmsmail_fab;
static struct RAB vmsmail_rab;

/* Possible returns from routine that reads VMSMAIL record. */

typedef enum { no_such_record, no_messages, forwarded, normal } VMSMAIL_STAT;


/*** MAIL.MAI file ***/

/*  The following information is taken from the VMS 4.4 fiche,
    facility MAIL, file MAILDEF.SDL, and file MAIL$ISAM_SUBS,
    routine DECODE_MISCDATA. */

#define MAI_DEFAULT     "MAIL.MAI"
#define NEWMAIL_FOLDER  "NEWMAIL"
#define MAX_MAI_RECORD  2048

static struct FAB mai_fab;
static struct RAB mai_rab;

static char mai_file[NAM$C_MAXRSS + 1]; /* name of current mail file */

/*  The latter part of a record in MAIL.MAI (of the type we're going
   to be reading) contains a sequence of Mail Message Header (MMH)
   records, not necessarily in any special order, their contents
   identified by one of the MMH_C constants that follows. */

/* Selected Mail Message Header constants. */

#define MMH_C_FROM              0       /* From: */
#define MMH_C_SUBJ              2       /* Subject: */
#define MMHFLAGS_M_NEWMSG       1       /* New message? */

/* Format of an MMH record.  mmh_data is only the start of the data:
   the mmh_length field specifies the data's length, in bytes.  In
   FROM, TO, and SUBJ records the data is a string of varying length;
   in NREC records the data is a longword integer.  I haven't seen
   records of the other types.  Add mmh_length to the address of
   mmh_data to get the start of the next MMH record. */

struct mmh_record {
        short mmh_type;
        short mmh_length;
        char mmh_data;
};

/*** Miscellaneous global variables ***/

/* NODE_NAMES is the start of a string that lists the nodes that
   messages from us might be from.  In a homogeneous cluster, our
   username really stands for the same individual on several nodes;
   but in an ordinary DECnet network, you may not have such
   consistency.  This string is defined in an external module, as
   it's site-specific and the site may not have a C compiler.
   It consists of counted ASCII strings, concatenated, with a null
   byte at the end. */

globalref char NODE_NAMES;

/*(tws)LOGICAL_CONVERSION -  Various  definitions and structures for use
  with the LOGICAL_CONVERSION(tranlog) routine added by TSCHMITZER.
  Refer to documentation in the main() in tranlog(). */

char *tranlog(char *s);

#define LNM$M_CASE_BLIND 33554432
#define LNM$_STRING 2

struct dsc {    long int len;           /* descriptors needed sometimes */
                char     *addr;         /* using only a len and address */
           };                           /* instead of the whole descrip */

union  pointer  {                       /* defines pointer type used in */
                int  *intbuf;           /* in item lists of certain sys */
                char *charbuf;          /* routines and stuff like that */
                };

struct itmlst   {
                unsigned short int bufferlength;
                unsigned short int item_code;
                union pointer  addr;
                int  *retlength;
                };                      /* used in some system services */


struct itmlst trnlist[2];               /* Used in translating logicals */

/*(tws)End of addition */
/* (tws) Save privs of user using unread to determine if should see all..*/

static int priv=0;

unsigned long int       user_holds_identifier(un,ri)
char    *un,*ri;
{
unsigned long   status, rid, foundrid, context, user[2];
char    rightsidentifier[128], username[128];

$DESCRIPTOR(rid_d, rightsidentifier);
$DESCRIPTOR(user_d, username);

    strcpy(rightsidentifier, ri);
    rid_d.dsc$w_length = strlen(ri);
    strcpy(username, un);
    user_d.dsc$w_length = strlen(un);

    status = sys$asctoid (
                 &rid_d,
                 &rid,
                 0 );

    if ( status != SS$_NORMAL ) return ( status );

    status = sys$asctoid (
                 &user_d,
                 &user[0] ,
                 0 );

    if ( status == SS$_NOSUCHID )
        {
            status = lib$getjpi (
                         &JPI$_UIC,
                         0 ,
                         0 ,
                         &user[0] ,
                         0 ,
                         0 );
        }

    if ( status != SS$_NORMAL ) sys$exit ( status );

    user[1] = 0;
    context = 0;

    while ( ( status = sys$find_held (
                            &user[0],
                            &foundrid ,
                            0 ,
                            &context ) ) == SS$_NORMAL)
        {
            if (foundrid == rid)
                {
                    return ( SS$_NORMAL );
                }
        }

    if ( status == SS$_NOSUCHID ) return ( SS$_ABORT );

    return ( status );
}

int     should_see_all_newmail()
{
    unsigned long int   status;
    long int itemcode=JPI$_USERNAME;
    char    username[UAF$S_USERNAME + 1];
    int     username_len;

$DESCRIPTOR(username_d, username);

    status = lib$getjpi (
                 &itemcode,
                 0 ,
                 0 ,
                 0 ,
                 &username_d ,
                 &username_len );

    if (status != SS$_NORMAL) return (FALSE);

    strcpy(strpbrk(username," "),"\0");

    status = user_holds_identifier(username, "ACC_ADMIN");

    if ( status == SS$_NORMAL )
        return(TRUE);

    if (getegid() < 32)
        return(TRUE);

    return (FALSE);
}

/*(tws)End addition */

/**************************/
main()
{
/*(tws)LOGICAL_CONVERSION - added logaddr to hold address of converted
  logical string (originally contained in 'firstname_buf' and x as a
  temporary variable for counting.. */

        char *logaddr;
        int x=0;
        $DESCRIPTOR(privqual_desc, "PRIV");
/*(tws)End addition */

        long cli$present();
        long cli$get_value();
        get_ourname();
        open_vmsmail();

        if (cli$present(&privqual_desc) == CLI$_PRESENT)
            priv = should_see_all_newmail();
        else
            priv = 0;

        /* Main loop, executed once for each user specified. */

        while (cli$get_value (&username_entity,
                &firstname_desc, &firstname_len) != CLI$_ABSENT ) {

/*(tws)***********************************************************************
        LOGICAL_CONVERSION
        WRITTEN BY: TSCHMITZER@SCU    some code borrowed(modified)
        PURPOSE: Checks to see if a string is a logical and if
                 so converts it to its literal value.
        PASS: Pass the string suspected of being a logical.
        RETURN: Pointer to a string.(NULL STRING if not a logical).
        REFER: tranlog() procedure docmentation and various throughout code.
*/
        /* Check to see if string is a logical, if so then convert */

        strncpy(savefirstname, firstname_buf, firstname_len);
        savefirstname[firstname_len] = '\0';

        logaddr = tranlog(firstname_desc.dsc$a_pointer);

        /* If string != logical, NULL string is returned */
        /* Copy string to descriptor, and pad with spaced so len=32. */
        if (strcmp(logaddr,"") != 0)
                strcpy(firstname_desc.dsc$a_pointer,logaddr);
        firstname_len = strlen(firstname_desc.dsc$a_pointer);
        for(x=firstname_len; x<31; ++x)
          firstname_desc.dsc$a_pointer[x]=' ';
        firstname_desc.dsc$a_pointer[x+1]='\0';

        /* (tws)End of LOGICAL_CONVERSION ******************************/

        /* If a username is too long for the buffer,
           CLI$GET_VALUE truncates it but returns the full
           length in firstname_len. */

        if (firstname_len > firstname_desc.dsc$w_length) {
          printf("\nUsername %.*s... is too long.\n",
                 firstname_desc.dsc$w_length,
                 firstname_desc.dsc$a_pointer);
          continue;
        }
        if (firstname_len <= 0)         /* ignore null usernames */
          continue;

        /* Find out where the user's mail file is (complicated
           by the possibility of forwarding). */

        str$upcase(&firstname_desc, &firstname_desc);
        str$copy_dx(&username_desc, &firstname_desc);
        forwardings = 0;
        username_len = 0;

        find_mai_file();
      } /* while */
} /* main() */

/*(tws)LOGICAL_CONVERSION - (TRANLOG)
        PURPOSE: Convert string passed to it from logical to literal
                 value if needed.
        INPUT: Pointer of a string.
        RETURNS: Pointer to converted string(NULL if non -logical).
        DATE: 8/9/88
        BY: TSCHMITZER@SCU (Descriptor info borrowed from reply.c)
*/

char *tranlog(s)
char s[];

{
$DESCRIPTOR(tab0desc,"LNM$SYSTEM_TABLE");
$DESCRIPTOR(tab1desc,"LNM$PROCESS_TABLE");
struct dsc name;
char *cp;
int loglen,status;
int attr_arg;

        s[strcspn(s," ")]= '\0';                /* terminate string(tws) */

        cp = malloc( 256 );                     /* go get us some memory */
        name.addr = s;                          /* address for string... */
        name.len  = strlen(s);                  /* length of the strings */
        attr_arg = LNM$M_CASE_BLIND;            /* don't care about case */
        trnlist[0].bufferlength = 100;          /* set up an itemlist... */
        trnlist[0].item_code    = LNM$_STRING;  /* say it is a string... */
        trnlist[0].addr.charbuf = cp;           /* buffer for the result */
        trnlist[0].retlength    = &loglen;      /* the length for result */
        trnlist[1].bufferlength = 0;
        trnlist[1].item_code    = 0;
        status = sys$trnlnm(&attr_arg,&tab0desc,&name,0,&trnlist);
        if (status != SS$_NORMAL) status = sys$trnlnm(&attr_arg,&tab1desc,&name,
   0,&trnlist);
        if ( status == SS$_NOLOGNAM || ( status & 1 ) == 0) loglen = 0;
        *(cp + (loglen&255)) = 0;               /* null terminate result */
        return(cp);                             /* and return the result */
}

/*(tws)End attition */

/**************************/
/*  Get the username of the process that's running this program. */

get_ourname()
{
        static struct {
                short buffer_length;
                short item_code;
                char *buffer_address;
                char *return_length_address;
                long null;
        } itemlist = {
                UAF$S_USERNAME, JPI$_USERNAME, 0, 0, 0
        };
        itemlist.buffer_address = ourname_buf;

        sys$getjpiw ( 0, 0, 0, &itemlist, 0, 0, 0 );
}
/**************************/
/*  Open the VMSMAIL.DAT file.  Aborts the program if unsuccessful,
    since we can't do anything without this file. */

open_vmsmail()
{
        long status;

        vmsmail_fab = cc$rms_fab;
        vmsmail_fab.fab$b_fac = FAB$M_GET;
        vmsmail_fab.fab$l_fna = &VMSMAIL_NAME;
        vmsmail_fab.fab$b_fns = sizeof ( VMSMAIL_NAME );
        vmsmail_fab.fab$l_dna = &VMSMAIL_DEFAULT;
        vmsmail_fab.fab$b_dns = sizeof ( VMSMAIL_DEFAULT );
        vmsmail_fab.fab$b_acmodes = PSL$C_EXEC << FAB$V_LNM_MODE;
        vmsmail_fab.fab$b_org = FAB$C_IDX;
        vmsmail_fab.fab$b_shr = FAB$M_SHRPUT | FAB$M_SHRGET |
                                FAB$M_SHRDEL | FAB$M_SHRUPD;

        status = sys$open(&vmsmail_fab);

        if (status != RMS$_NORMAL)
                exit(status);

        vmsmail_rab = cc$rms_rab;
        vmsmail_rab.rab$l_fab = &vmsmail_fab;
        vmsmail_rab.rab$l_rop = RAB$M_LOC | RAB$M_NLK | RAB$M_RRL;
        vmsmail_rab.rab$b_krf = 0;
        vmsmail_rab.rab$l_kbf = username_buf;
        vmsmail_rab.rab$b_ksz = V5VMSMAIL_KEY_SIZE;
        vmsmail_rab.rab$b_rac = RAB$C_KEY;
        vmsmail_rab.rab$w_usz = V5MAX_VMSMAIL_RECORD;

        status = sys$connect(&vmsmail_rab);

        if (status != RMS$_NORMAL)
                exit(status);
}
/**************************/
/*  Given global variable "username", looks for the MAIL.MAI file.
    Returns 0 if it should be called again to process a forwarded
    username, 1 if all done for whatever reason. */

find_mai_file()
{
        char vmsmail_dir[NAM$C_MAXRSS + 1];
        VMSMAIL_STAT get_vmsmail_data();

        *mai_file = '\0';

        /* The only non-obvious case is "no such record": this is
           perfectly legal if the user in question has never sent
           or received mail.
             The message count in VMSMAIL.DAT can have the wrong value,
           but the error is usually that it's > 0 when there are no
           messages to be read.  Thus it's generally safe to return
           without even bothering to open MAIL.MAI if that count is
           zero, as we do here. */

        get_vmsmail_data(vmsmail_dir);

        return(1);
}
/**************************/
/*  Read the VMSMAIL record for a given username. */

VMSMAIL_STAT get_vmsmail_data(vmsmail_dir)
char *vmsmail_dir;
{
        char vmsmail_data[V5MAX_VMSMAIL_RECORD + 1];
        struct v5vmsmail_record *data;
        long status, new_len;
        int i,p;
        unsigned short *fldtype,*fldlen;
        char *fldname;

        *vmsmail_dir = '\0';

        vmsmail_rab.rab$l_ubf = vmsmail_data;

        status = sys$get(&vmsmail_rab);

        if (status == RMS$_RNF)
                return(no_such_record);
        if (status != RMS$_NORMAL && status != RMS$_OK_RRL)
                exit(status);

        data = (struct v5vmsmail_record *)(vmsmail_rab.rab$l_rbf);

        for (i=0; i<31; i++)
          printf("%c",data->vmsmail_username[i]);
        printf("\nNew mail cnt:  %d\n\n",data->mailcnt);
        printf("Mail account flags:   %4X  (hex)\n",data->flags);
        if (data->flags &
            (VMSMAIL_CS_SEND|VMSMAIL_CS_REPLY|VMSMAIL_CS_FORWARD)
           ) {
          printf("    Copy to self on:  ");
          if (data->flags & VMSMAIL_CS_SEND)
            printf("send  ");
          if (data->flags & VMSMAIL_CS_REPLY)
            printf("reply  ");
          if (data->flags & VMSMAIL_CS_FORWARD)
            printf("forward");
          printf("\n");
        } /* if */
        if (data->flags & VMSMAIL_NOAUTOPURGE)
          printf("    Auto-purge Disabled\n");
        if (data->flags & VMSMAIL_CC_PROMPT)
          printf("    Carbon Copy Prompting Enabled\n");
        printf("\n");

        for (p=0;;p=p+*fldlen+4) {
          fldtype = &data->field[p];
          if (!*fldtype) break;
          fldlen = &data->field[p+2];
          fldname = &data->field[p+4];

          switch (*fldtype) {
            case 3:
              printf("Mail directory:  ");
              break;
            case 4:
              printf("Forward mail to:  ");
              break;
            case 5:
              printf("Personal name:  ");
              break;
            case 9:
              printf("Default print queue:  ");
              break;
            case 13:
              printf("Default queue form:  ");
              break;
          } /* switch */

          for (i=0; i<*fldlen; i++,fldname++)
            printf("%c",*fldname);
          printf("\n");
        } /* for */

} /* get_vmsmail_data() */
========================  VMSMAIL.CLD =======================================

define verb vmsmail
        image "ACCLIB:[LOCAL.UNREAD]VMSMAIL"
        parameter p1, prompt="To",
                value ( required, list ), label=username
        qualifier PRIV, nonnegatable
========================  VMAKE.COM  ========================================
$! Make file to build VMSMAIL program.
$!   1.  Edit NODENAMES.MAR to contain your nodenames.
$!   2.  Run this com file to compile and link.
$!   3.  Edit VMSMAIL.CLD to point to the correct file and then do a
$!       SET COMMAND VMSMAIL.
$!
$ cc/nolis/nodebug vmsmail
$ macro/nolis/nodebug nodenames
$ link/nomap/notrace vmsmail, nodenames, sys$input:/options
sys$share:vaxcrtl/share
=============================================================================
The end

