Contributor: GREG ESTABROOKS          


{***********************************************************************}
{$M 16384,0,0}                  { Save memory for Calling PKUNZIP.      }
PROGRAM NMembers;               { May 16/94, Greg Estabrooks.           }
USES
     DOS;
CONST
     Ver      = 'V0.2á';        { Current Version of program.           }
     ProgTitle= 'NMem '+Ver+'- Conference Member Tracking Program. ';
     Author   = 'CopyRight (C) 1994, Greg Estabrooks.';
TYPE
    Direction = (Left,Right);

    MsgDHdr = RECORD            { Structre of QWK Message Header.       }
               Status    :CHAR;
               MNum      :ARRAY[1..7] OF CHAR;
               Date      :ARRAY[1..8] OF CHAR;
               Time      :ARRAY[1..5] OF CHAR;
               MTo       :ARRAY[1..25] OF CHAR;
               MFrom     :ARRAY[1..25] OF CHAR;
               MSubj     :ARRAY[1..25] OF CHAR;
               Pass      :ARRAY[1..12] OF CHAR;
               MRefer    :ARRAY[1..8] OF CHAR;
               NChunks   :ARRAY[1..6] OF CHAR;
               Active    :CHAR;
               MConf     :WORD;
               Fill      :ARRAY[1..3] OF CHAR;
             END;{MsgDHdr}

    NDXType = RECORD
                Offset :LONGINT;
                Misc   :BYTE;
              END;

    MessInfType = RECORD
                   Name  :STRING[25];   { Name of person message FROM.  }
                   Origin:STRING[80];   { Origin line from message.     }
                  END;

VAR
  QWKName :STRING[128];         { QWK File to process.                  }
  OutFile :STRING[128];         { File to place new member names.       }
  WorkDir :STRING[128];         { Holds the name of our work directory. }
  OutHan  :TEXT;                { File handle for output file.          }
  MessDat :FILE;                { File handle for MESSAGES.DAT.         }

  NumMess :WORD;                { Number of messages in conference.     }
  NewMems :WORD;                { Number of new members found.          }
  NumFound:WORD;                { Holds number of different names found.}

  fOfs    :ARRAY[1..500] OF LONGINT;{ Holds offset info from NDX file.  }
  FInf    :ARRAY[1..500] OF MessInfType;

FUNCTION PadStr( Dir :Direction; Str2Pad :STRING; Til :BYTE;
                                                 Ch :CHAR ) :STRING;
                         { Function to pad a string with 'Ch' until it  }
                         { is 'Til' long.                               }
VAR
   Temp :STRING;                { Temporary String info.                }
BEGIN
  Temp := Str2Pad;              { Initialize 'Temp' to users string.    }
  IF Length(Temp) < Til THEN    { If its smaller than 'Til' add padding.}
  WHILE (Length(Temp) < Til) DO { Loop until proper length reached.     }
   BEGIN
     CASE Dir OF
      Right :Temp := Temp + Ch; { If Right then add to end of string.   }
      Left  :Temp := Ch + Temp; { If Left then add to begining.         }
     END;
   END;
  PadStr := Temp;               { Return proper result.                 }
END;

PROCEDURE InitVars;
                        { Procedure to initialize program variables.    }
VAR
   Temp :STRING[4];             { Temporary String value.               }
BEGIN
  FillChar(FInf,SizeOf(FInf),#0); { Clear FInf.                         }
  NumMess := 0;                 { Clear number of messages.             }
  NewMems := 0;                 { Clear number of new members found.    }
  QWKName := ParamStr(1);       { Get QWK Name from command line.       }
  NumFound := 1;                { Initialize 'NumFound.'                }
  Temp := ParamStr(2);          { Get Conf Number from command line.    }
  OutFile := 'CNF'+PadStr(Left,Temp,5,'0')+'.LST';
                                { Prepare output file name.             }

  GetDir(0,WorkDir);            { Save current directory.               }
  IF WorkDir[Length(WorkDir)] = '\' THEN
   WorkDir := WorkDir +'NMEM'
  ELSE
   WorkDir := WorkDir +'\NMEM';
END;{InitVars}

PROCEDURE Syntax_Error;
                       { Display proper command line syntax to user.    }
BEGIN
  Writeln;                      { Skip a line.                          }
  Writeln(
     'Syntax: NMEM [drive]:[path]PacketName ConfNum');
                                { Show syntax for user.                 }
  Writeln;                      { Skip a line.                          }
                                { Show an example usage.                }
  Writeln('EXAMPLE : NMEM C:\QWK\MYBBS.QWK 123');
  Writeln(' Scans MYBBS.QWK and generates CNF00123.LST');
  Halt(1);                      { Halt program with and ERRORLEVEL of 1.}
END;{Syntax_Error}

FUNCTION fExist( FName :STRING ) :BOOLEAN;
                        { Routine to determine whether or not 'FName'   }
                        { really exists.                                }
BEGIN
  fExist := (fSearch(FName,'') <> '');
END;{fExist}

FUNCTION DirExist( DName :STRING ) :BOOLEAN;
                         { Routine to determine whether or not the      }
                         { Directory 'DName' exists.                    }
VAR
   DirInf :SearchRec;           { Hold info if dir found.               }
BEGIN
  FindFirst(DName,Directory,DirInf);
  DirExist := (DosError = 0);
END;{DirExist}

PROCEDURE DelWorkDir;
                         { Routine to delete files in the work directory}
                         { and them remove the directory.               }
VAR
   FileInf :SEARCHREC;          { Holds file names for erasure.         }
   fVar    :FILE;               { Handle of file to delete.             }
BEGIN
  FindFirst(WorkDir+'\*.*',Archive,FileInf); { Get File Name.           }
  WHILE (DosError = 0) DO       { Loop until all file names read.       }
  BEGIN
    Assign(fVar,WorkDir+'\'+FileInf.Name);
                                { Assign file name to handle.           }
    Erase(fVar);                { Erase File.                           }
    FindNext(FileInf);          { Get next file name.                   }
  END;
END;{DelWorkDir}

PROCEDURE OpenPacket( QName :STRING );
                         { Routine open mail packets.                   }
VAR
   PKPath :STRING;              { Holds location of PKUNZIP.EXE         }
BEGIN
  IF NOT DirExist(WorkDir) THEN { If dir doesn't exist then make it.    }
   BEGIN
    {$I-}                       { Turn I/O checking off.                }
    MKDir(WorkDir);             { Create our work directory.            }
    {$I+}                       { Turn I/O checking off.                }

    IF IOResult <>0 THEN        { If I/O error then                     }
     BEGIN                      { Display error message.                }
      Writeln('Error creating work directory',^G);
      Halt(1);                  { Now halt program.                     }
     END;

   END
  ELSE
   DelWorkDir;                  { If it does exist then clear it.       }

  IF NOT fExist('PKUNZIP.EXE') THEN { If it's not in the current dir    }
   BEGIN                        { then search the %PATH%.               }
    PKPath := fSearch('PKUNZIP.EXE',GetEnv('PATH'));
    IF PKPath = '' THEN         { If it's nowhere to be found then      }
     BEGIN                      { Display error message.                }
      Writeln('Cannot find PKUNZIP.EXE!',^G);
      Writeln('It must be located either in the ',
                 'current directory or along your %PATH%');
      Halt(1);                  { Now halt program.                     }
     END;
   END;

  SwapVectors;                  { Swap to proper Interrupt vectors.     }
  Exec(GetEnv('COMSPEC'),'/C '+PKPath+' '+QWKName+' '+WorkDir+' >NUL');
  SwapVectors;                  { Swap em back.                         }

  IF DosError <> 0 THEN         { If there was an 'Exec' error then     }
   BEGIN                        { Display error message.                }
    Writeln('Error #',DosError,' occured executing ',PKPath,^G);
    Halt(1);                    { Now Halt program.                     }
   END;

  IF DosExitCode <> 0 THEN      { Check for a program error.            }
   Writeln(PKPath,' returned an ERRORLEVEL of ',DosExitCode,^G);
END;{OpenPacket}

FUNCTION NotNumber( NumStr :STRING ) :BOOLEAN;
                         { Routine to determine whether or not 'NumStr' }
                         { is a valid number.                           }
                         { Returns TRUE if not a number FALSE if a num. }
VAR
   Result :BOOLEAN;             { Holds Function result.                }
   StrPos :BYTE;                { Position withing string.              }
BEGIN
  Result := FALSE;              { Defaults to false.                    }
  FOR StrPos := 1 TO Length(NumStr) DO { Loop through entire string.    }
   IF NOT (NumStr[StrPos] IN
            ['0','1','2','3','4','5','6','7','8','9']) THEN
     Result := TRUE;

  NotNumber := Result;          { Return proper result.                 }
END;{NotNumber}

FUNCTION ReadNDX :BOOLEAN;
                         { Routine to read proper NDX file for conference.}
VAR
   Result :BOOLEAN;             { Holds Function result.                }
   NDX    :FILE;                { File handle for NDX file.             }
   Info   :NDXType;             { Hold info read from NDX file.         }
   NumRead:WORD;                { Holds number of bytes read from NDX.  }
BEGIN
  Result := TRUE;               { Default to success.                   }
  Assign(NDX,WorkDir+'\'+PadStr(Left,ParamStr(2),3,'0')+'.NDX');
  {$I-}                         { Turn off I/O checking.                }
  Reset(NDX,1);                 { Open NDX for reading.                 }
  {$I+}                         { Turn on I/O checking.                 }
  WHILE NOT EOF(NDX) DO         { Loop until end of file.               }
   BEGIN
     BlockRead(NDX,Info,SizeOf(Info),NumRead); { Read offset of message.}
     IF (NumRead = Sizeof(Info)) AND (NumMess <501) THEN
                                { If proper amount read then            }
      BEGIN                     { Convert it to a proper LONGINT.       }
        INC(NumMess);           { Increase message total.               }
                                { Now convert offset.                   }
        fOfs[NumMess] := ((Info.Offset AND NOT $FF000000) OR $00800000)
                          SHR (24 - ((Info.Offset SHR 24) AND $7F));
        fOfs[NumMess] := (fOfs[NumMess]-1) SHL 7;
      END
     ELSE
      Result := FALSE;          { Otherwise return FALSE result.        }
   END;
  Close(NDX);                   { Close NDX File.                       }
  ReadNDX := Result;            { Return proper result.                 }
END;{ReadNDX}

PROCEDURE RemoveSpaces(VAR Str2Rem :STRING);
                         { Routine to remove any spaces from 'Str2Rem'. }
VAR
   StrPos :WORD;                { Position within string.               }
   Temp   :STRING;              { Temporary string work space.          }
BEGIN
  Temp := '';                   { Clear string.                         }
  FOR StrPos := 1 TO Length(Str2Rem) DO { Loop through all characters.  }
   IF Str2Rem[StrPos] <> #32 THEN   { If its not a space then           }
    Temp := Temp + Str2Rem[StrPos]; { add it to our string.             }

  Str2Rem := Temp;              { Return newly changed string.          }
END;{RemoveSpaces}

FUNCTION Compare( Str1,Str2 :STRING ) :BOOLEAN;
                         { Routine to compare to strings after removing }
                         { any spaces from it.Case INSENSITIVE.         }
VAR
   Result :BOOLEAN;             { Result from comparing.                }
   StrPos :BYTE;                { Position within 2 strings.            }
BEGIN
  Result := TRUE;               { Default result to TRUE.               }
  RemoveSpaces(Str1);           { Trim spaces from the strings.         }
  RemoveSpaces(Str2);
  IF Length(Str1) <> Length(Str2) THEN { If different lengths then they }
   Result := FALSE                     { must be different.             }
  ELSE
   BEGIN
    StrPos := 0;                { Initialize 'StrPos' to 0.             }
    REPEAT                      { Loop until every char checked.        }
     INC(StrPos);               { Point to next char.                   }
     IF UpCase(Str1[StrPos]) <> UpCase(Str2[StrPos]) THEN
      BEGIN
       Result := FALSE;         { If there not the same then return     }
                                { a FALSE result.                       }
       StrPos := Length(Str2);  { Now set loop exit condition.          }
      END;
    UNTIL StrPos = Length(Str2);
   END;

  Compare := Result;            { Return proper result.                 }
END;{Compare}

FUNCTION Arr2String( VAR Arr; Len :BYTE ) :STRING;
                         { Routine to convert 'Len' bytes of the array  }
                         { 'Arr' into a string.                         }
VAR
   Result :STRING;              { Holds function result.                }
BEGIN
  MOVE(Arr,Result[1],Len);      { Move bytes into our result string.    }
  Result[0] := CHR(Len);        { Set string length byte.               }

  Arr2String := Result;         { Return proper result.                 }
END;{Arr2String}

FUNCTION Fmt( Info :WORD ) :STRING;
                         { Routine to create a String with info int the }
                         { format '00'.                                 }
VAR
   Temp :STRING;                { Hold temporary string info.           }
BEGIN
  Str(Info,Temp);               { Convert info to a string.             }
  IF Length(Temp) = 1 THEN      { if its only a single digit then add   }
    Fmt := '0'+Temp             { leading zero.                         }
  ELSE
    Fmt := Temp;
END;{Fmt}

FUNCTION TimeStr :STRING;
VAR
   Hour,Min,Sec,Sec100 :WORD;   { Holds temporary time info.            }
   Year,Mon,Day,DoWeek :WORD;   { Holds temporary date info.            }
   TempTime :STRING;            { Holds temporary TimeStr.              }
BEGIN
  GetDate(Year,Mon,Day,DoWeek);
  TempTime := Fmt(Mon)+'-'+Fmt(Day)+'-'+Fmt(Year-1900)+' at ';
  GetTime(Hour,Min,Sec,Sec100); { Get Current Time.                     }
  IF Hour >= 12 THEN
    TempTime := TempTime+Fmt(Hour-12)+':'+Fmt(Min)+'pm'
  ELSE
    IF Hour = 0 THEN
      TempTime := TempTime+'12:'+Fmt(Min)+'am'
    ELSE
      TempTime := TempTime+Fmt(Hour)+':'+Fmt(Min)+'pm';
  TimeStr := TempTime;
END;

FUNCTION GetOrigin( Chunks :WORD ) :STRING;
                         { Routine to get message origin line if any.   }
VAR
   Result :STRING;              { Holds function result.                }
   CurChnk:WORD;                { Holds current chunk being read.       }
   BufPos :WORD;                { Position within buffer.               }
   Temp   :STRING;
   NumRead:WORD;                { Holds number of bytes read from file. }
   Buffer :ARRAY[1..128] OF CHAR;{ Buffer for info read from file.      }
   TareLin:BOOLEAN;             { Holds whether or not we've past the   }
                                { tear line.                            }
BEGIN
  Result := '';                 { Clear result.                         }
  Temp := '';                   { Clear temporary storage space.        }
  TareLin := FALSE;             { Default to FALSE.                     }
  FOR CurChnk := 1 TO Chunks-1 DO { Loop through all the 128 byte chunks.}
   BEGIN
    BlockRead(MessDat,Buffer,128,NumRead); { Read message info.         }
    FOR BufPos := 1 TO 128 DO
     BEGIN
      IF Buffer[BufPos] = #227 THEN
       BEGIN
        IF Temp = '---' THEN
         TareLin := TRUE
        ELSE
         IF TareLin AND (Temp <> PadStr(Right,'',Length(Temp),' ')) THEN
          Result := Temp;
        Temp := ''
       END
      ELSE
       Temp := Temp + Buffer[BufPos];
     END;
   END;

  IF (Result = '') OR (Pos('ILink:',Result) = 0) THEN
   Result := ' þ Origin Line Unavailable þ ';

  GetOrigin := Result;          { Return proper result.                 }
END;

PROCEDURE ReadMsgs;
                         { Routine to read Messages and save new members}
                         { to disk.                                     }
VAR
  MessBuf :MsgDHdr;             { Holds header info read from file.     }
  InfPos  :WORD;                { Loop variable for searching 'FileInf'.}
  CurMess :WORD;                { Loop variable for reading messages.   }
  NumRead :WORD;                { Holds number of bytes read from file. }
  Found   :BOOLEAN;             { Holds whether or not name was already }
                                { read.                                 }
  FoundPos:WORD;                { Holds position in array name was found.}
  Temp    :STRING;              { Holds temporary string info.          }
  Chunks  :WORD;                { Holds number of 128 byte chunks message}
                                { takes up in file.                     }
  ErrCode :WORD;                { Holds error codes returned from 'Val'.}
  Create  :BOOLEAN;
BEGIN
  Create  := NOT fExist(OutFile);
  IF NumFound = 0 THEN NumFound := 1;
  Assign(MessDat,WorkDir+'\MESSAGES.DAT');{ Assign handle to message file.}
  {$I-}                         { Turn I/O checking off.                }
  Reset(MessDat,1);             { Open file for reading.                }
  {$I+}                         { Turn I/O checking back on.            }
  FOR CurMess := 1 TO NumMess DO { Loop through all the messages.       }
   BEGIN
     Seek(MessDat,fOfs[CurMess]);{ Move to current message position.    }
     BlockRead(MessDat,MessBuf,SizeOf(MessBuf),NumRead); { Read Header. }
     FOR InfPos := 1 TO NumFound DO
      BEGIN
       Found := Compare(FInf[InfPos].Name,Arr2String(MessBuf.MFrom,25));
       IF Found THEN
        InfPos := NumFound;
      END;
     IF NOT Found THEN
      BEGIN
       INC(NewMems);            { Increase number of new members.       }
       IF Create AND (NumFound = 1) THEN
        BEGIN
          NumFound := 0;
          Create := FALSE;
        END;
       INC(NumFound);           { Increase number found.                }

       FInf[NumFound].Name := Arr2String(MessBuf.MFrom,25);
       Temp := Arr2String(MessBuf.NChunks,6);
       RemoveSpaces(Temp);
       Val(Temp,Chunks,ErrCode);
       FInf[NumFound].Origin := GetOrigin(Chunks);
      END;
   END;
  Close(MessDat);               { Close message file.                   }
END;{ReadMsgs}

PROCEDURE SaveList;
                         { Routine to write our list to the list file.  }
VAR
   ListPos :WORD;               { Position withing list being written.  }
BEGIN
  Assign(OutHan,OutFile);       { Assign handle to file name.           }
  {$I-}                         { I/O off.                              }
  Rewrite(OutHan);              { Open file for writing.                }
  {$I+}                         { I/O on.                               }
  IF IOResult <> 0 THEN         { If there was an error.                }
   Writeln('-Error! Unable to Open ',OutFile,^G)
  ELSE
   BEGIN
    Writeln(OutHan,'');         { Write a blank line to file.           }
    Writeln(OutHan,'/*'+PadStr(Right,'',75,'-')+'*/');

    Writeln(OutHan,'                         Conference [',ParamStr(2),
                   '] Members list.');
    Writeln(OutHan,PadStr(Right,'',24,' ')+'Last Change '+TimeStr);
    Writeln(OutHan,'/*'+PadStr(Right,'',75,'-')+'*/');
    FOR ListPos := 1 TO NumFound DO
     BEGIN
      Writeln(OutHan,'');       { Writeln a blank line.                 }
      Writeln(OutHan,'User : '+FInf[ListPos].Name); { Writeln user name.}
      Writeln(OutHan,FInf[ListPos].Origin); { Write users origin line.  }
     END;
    Close(OutHan);                { Close file.                           }
   END;
END;{SaveList}

PROCEDURE ReadList;
                         { Routine to read in the conf members list.    }
VAR
   InFile :TEXT;                { Text handle for conference list.      }
   Temp   :STRING;              { Holds string read from file.          }
BEGIN
  NumFound := 0;
  Assign(InFile,OutFile);       { Assign handle to file name.           }
  {$I-}                         { I/O checking off.                     }
  Reset(Infile);                { Open file for reading.                }
  {$I+}                         { I/O checking on.                      }
  WHILE (NOT EOF(InFile)) AND (NumFound <500) DO
   BEGIN
    ReadLn(InFile,Temp);        { Read a line from the file.            }
    IF Copy(Temp,1,7) = 'User : ' THEN { If its the user name then.     }
     BEGIN                      { Save Name to array and read origin.   }
      INC(NumFound);
      FInf[NumFound].Name := Copy(Temp,8,Length(Temp));
      ReadLn(InFile,FInf[NumFound].Origin);
     END;
   END;
  IF NumFound = 0 THEN
   NumFound := 1;
  Close(InFile);                { Close file.                           }
END;{ReadList}

PROCEDURE SortList;
                         { Routine to sort the list of conference       }
                         { members using a simple bubble sort.          }
VAR
   Temp  :MessInfType;          { Temporary record for swapping.        }
   Index1,Index2:WORD;          { Sort loop variables.                  }
BEGIN
  FOR Index1 := NumFound DOWNTO 1 DO
   FOR Index2 := 2 TO Index1 DO
    IF FInf[Index2-1].Name > FInf[Index2].Name THEN
     BEGIN
      Temp := FInf[Index2];
      FInf[Index2] := FInf[Index2-1];
      FInf[Index2-1] := Temp;
     END;
END;{SortList}

BEGIN
  Writeln(ProgTitle);           { Display program title.                }

  IF (ParamCount <> 2) OR NotNumber(ParamStr(2)) THEN
                                { If wrong command argument show proper }
    Syntax_Error                { syntax to use.                        }
  ELSE
   BEGIN
    InitVars;                   { Initialize variables.                 }
    IF fExist(QWKName) THEN     { If it exists begin processing.        }
     BEGIN
      Writeln('-Opening Packet');
      OpenPacket(QWKName);      { Open mail packet.                     }
      IF fExist(WorkDir+'\'+PadStr(Left,ParamStr(2),3,'0')+'.NDX') THEN
       BEGIN                    { IF there are any Messages in conf then}
                                { Attempt to read NDX files.            }
        Writeln('-Reading NDX file');

        IF ReadNDX THEN         { IF there is no error, read messages.  }
         BEGIN

          IF fExist(OutFile) THEN{ IF Conf list already exist then read.}
           BEGIN
            Writeln('-Reading ',OutFile);
            ReadList;

           END;

          Writeln('-Reading Messages in conference [',ParamStr(2),']');
          ReadMsgs;

          IF NewMems > 0 THEN
           BEGIN

            IF fExist(OutFile) THEN
             BEGIN
              Writeln('-Renaming ',OutFile,' to ',Copy(OutFile,1,8)+'.BAK');

              IF fExist(Copy(OutFile,1,8)+'.BAK') THEN
               BEGIN
                Assign(OutHan,Copy(OutFile,1,8)+'.BAK');
                Erase(OutHan);
               END;

              Assign(OutHan,OutFile);
              Rename(OutHan,Copy(OutFile,1,8)+'.BAK');
             END
            ELSE
             BEGIN
              Writeln('-Creating ',OutFile);
              Assign(OutHan,OutFile);
              {$I-}
              Rewrite(OutHan);
              {$I+}
              Close(OutHan);
             END;
            Writeln('-Sorting member list');
            SortList;
            Writeln('-Saving ',NewMems,
                    ' new members for a total of ',NumFound,' members');
            SaveList;
           END
          ELSE
           Writeln('-No new members found');
         END
        ELSE
         Writeln('-Error reading NDX file',^G);
       END
      ELSE                      { Other wise let user know its not there.}
       Writeln('-NDX file for conference [',ParamStr(2),
               '] does not exist. No new messages?'^G);

      Writeln('-Deleteing Work Directory');
      DelWorkDir;               { Delete Work Directory.                }
      {$I-}                     { Turn I/O checking off.                }
      RMDir(WorkDir);           { Remove work Directory.                }
      {$I+}                     { Turn I/O checking on.                 }
      IF IOResult <> 0 THEN     { If and error occurs then              }
      Writeln('Error Removing work directory.',^G);

     END
    ELSE                        { .... Otherwise ......                 }
     BEGIN                      { Show error message and beep.          }
      Writeln(^G,'Cannot find ',QWKName);
      Writeln('Tracking aborted!!');
     END;
   END;
END.{NMembers}
{***********************************************************************}