-- FTPSysMail.mesa,  Edit: HGM July 28, 1980  10:32 PM  

-- Copyright  Xerox Corporation 1979, 1980

DIRECTORY
  FTPDefs,
  FTPPrivateDefs,
  Ascii USING [CR],
  Inline USING [LowHalf],
  String USING [AppendChar, AppendLongNumber, EquivalentString, StringToLongNumber],
  Storage USING [Node, Free, CopyString],
  Time USING [AppendCurrent];

FTPSysMail: PROGRAM
    IMPORTS Inline, String, Storage, Time, FTPDefs, FTPPrivateDefs
    EXPORTS FTPDefs
    SHARES FTPDefs, FTPPrivateDefs =
BEGIN OPEN FTPDefs, FTPPrivateDefs; 
 
-- **********************!  Types  !***********************

-- mail system state information
SysMailSystem: TYPE = POINTER TO SysMailSystemObject;
SysMailSystemObject: TYPE = RECORD [
  filePrimitives: FilePrimitives,
  fileSystem: FileSystem,
  directoryFileHandle, stagingFileHandle: FileHandle,
  messageLength: LONG INTEGER];

-- **********************!  Constants  !***********************

mailboxTerminator:		CHARACTER = '@;
fileTerminator:		CHARACTER = Ascii.CR;
dateAndTimeTerminator:		CHARACTER = ';;
messageLengthTerminator:	CHARACTER = Ascii.CR;

ftpsystem: POINTER TO FTPSystem = LocateFtpSystemObject[];

mailPrimitivesObject: MailPrimitivesObject ← [
  CreateMailSystem: CreateMailSystem,
  DestroyMailSystem: DestroyMailSystem,
  InspectCredentials: InspectCredentials,
  LocateMailboxes: LocateMailboxes,
  StageMessage: StageMessage,
  DeliverMessage: DeliverMessage,
  ForwardMessage: ForwardMessage,
  RetrieveMessages: RetrieveMessages];

-- **********************!  Variables  !***********************

useCount: CARDINAL ← 0;
mailboxQueueObject: QueueObject;

-- **********************!  Mail Foothold Procedure  !***********************

-- Note:  This mail system is constructed atop the client's file system.
--   Besides a scratch file in which the text of incoming messages is staged,
--   and a semi-permanent mailbox file for each legal mail recipient,
--   the following prefabricated, permanent file giving mailbox->filename correspondences
--   is employed:  FTPSysMail-Directory.Bravo

SysMailPrimitives,
SomeMailPrimitives: PUBLIC PROCEDURE RETURNS [mailPrimitives: MailPrimitives] =
  BEGIN
  -- return mail primitives
    mailPrimitives ← @mailPrimitivesObject;
  END;
 
-- **********************!  Mail Primitives  !***********************

CreateMailSystem: PROCEDURE [filePrimitives: FilePrimitives, bufferSize: CARDINAL] RETURNS [mailSystem: MailSystem, forwardingProvided: BOOLEAN] =
  BEGIN
  -- Note:  bufferSize expressed in pages; zero implies default.
  -- local constants
    stagingFile: STRING = [maxStringLength];
  -- local variables
    sysMailSystem: SysMailSystem;
  -- verify presence of file primitives
    IF filePrimitives = NIL THEN Abort[filePrimitivesNotSpecified];
  -- allocate and initialize mail system object
    sysMailSystem ← Storage.Node[SIZE[SysMailSystemObject]];
    sysMailSystem↑ ← SysMailSystemObject[
      filePrimitives: filePrimitives,
      fileSystem: NIL,
      directoryFileHandle: NIL, stagingFileHandle: NIL,
      messageLength: ];
    mailSystem ← LOOPHOLE[sysMailSystem];
  -- initialize mailbox queue
    IF (useCount ← useCount+1) = 1 THEN
      InitializeQueue[@mailboxQueueObject, String.EquivalentString];
  -- create file system
    BEGIN OPEN sysMailSystem; ENABLE UNWIND => DestroyMailSystem[mailSystem];
    fileSystem ← filePrimitives.CreateFileSystem[bufferSize];
  -- open mailbox directory file for read
    [directoryFileHandle, ] ←
      filePrimitives.OpenFile[fileSystem, "FTPSysMail-Directory.Bravo"L, read, FALSE, NIL];
  -- open staging file for writeThenRead
  -- Note:  stagingFile.length=0, requesting creation of scratch file.
    [stagingFileHandle,  ] ←
      filePrimitives.OpenFile[fileSystem, stagingFile, writeThenRead, FALSE, NIL];
  -- decline to forward messages
    forwardingProvided ← FALSE;
    END; -- enable
  END;

DestroyMailSystem: PROCEDURE [mailSystem: MailSystem] =
  BEGIN
  -- local constants
    sysMailSystem: SysMailSystem = LOOPHOLE[mailSystem];
  -- close staging file
  -- Note: aborted=TRUE, requesting deletion of scratch file.
    BEGIN OPEN sysMailSystem;
    IF stagingFileHandle # NIL THEN
      filePrimitives.CloseFile[fileSystem, stagingFileHandle, TRUE];
  -- close mailbox directory file
    IF directoryFileHandle # NIL THEN
      filePrimitives.CloseFile[fileSystem, directoryFileHandle, FALSE];
  -- destroy file system
    IF fileSystem # NIL THEN filePrimitives.DestroyFileSystem[fileSystem];
  -- finalize mailbox queue
    IF (useCount ← useCount-1) = 0 THEN FinalizeQueue[@mailboxQueueObject];
  -- release mail system object
    Storage.Free[sysMailSystem];
    END; -- open
  END;

InspectCredentials: PROCEDURE [mailSystem: MailSystem, status: Status, user, password: STRING] =
  BEGIN
  -- no operation
  END;

LocateMailboxes: PROCEDURE [mailSystem: MailSystem, localMailboxList: Mailbox] =
  BEGIN
  -- Note:  Skips mailboxes showing located=TRUE; allocates location from the heap.
  -- EnumerateMailboxes appendage
    NoteMailbox: PROCEDURE [definedMailbox, file: STRING] =
      BEGIN
      -- search mailbox list for defined mailbox
        FOR localMailboxList ← localMailboxListHead, localMailboxList.nextMailbox
            UNTIL localMailboxList=NIL DO OPEN localMailboxList;
          IF ~located AND String.EquivalentString[mailbox, definedMailbox] THEN
            BEGIN
            location ← Storage.CopyString[file];
            located ← String.EquivalentString[mailbox, definedMailbox];
            END;
          ENDLOOP;
      END;
  -- local constants
    sysMailSystem: SysMailSystem = LOOPHOLE[mailSystem];
    localMailboxListHead: Mailbox = localMailboxList;
  -- enumerate defined mailboxes
    EnumerateMailboxes[sysMailSystem, NoteMailbox];
  END;

StageMessage: PROCEDURE [mailSystem: MailSystem, receiveBlock: PROCEDURE [UNSPECIFIED, POINTER, CARDINAL] RETURNS [CARDINAL], receiveBlockData: UNSPECIFIED] =
  BEGIN
  -- Note:  Records messageLength.
  -- ReceiveBlock procedure
    ReceiveBlock: PROCEDURE [unused: UNSPECIFIED, destination: POINTER,
        maxWordCount: CARDINAL] RETURNS [actualByteCount: CARDINAL] =
      BEGIN OPEN sysMailSystem;
      -- receive block
        actualByteCount ← receiveBlock[receiveBlockData, destination, maxWordCount];
      -- increment message length
        messageLength ← messageLength + actualByteCount;
      END;
  -- local constants
    sysMailSystem: SysMailSystem = LOOPHOLE[mailSystem];
  -- stage message
    BEGIN OPEN sysMailSystem;
    messageLength ← 0;
    filePrimitives.WriteFile[fileSystem, stagingFileHandle, ReceiveBlock, NIL];
    END; -- open
  END;

DeliverMessage: PROCEDURE [mailSystem: MailSystem, localMailboxList: Mailbox] =
  BEGIN
  -- Note:  Assumes message text staged;
  --   skips mailboxes showing located=FALSE or delivered=TRUE;
  --   suppresses errors raised in individual delivery attempts.
  -- local constants
    sysMailSystem: SysMailSystem = LOOPHOLE[mailSystem];
    localMailboxListHead: Mailbox = localMailboxList;
    textualDateAndTime: STRING = [maxStringLength];
    textualMessageLength: STRING = [maxStringLength];
  -- note date and time and message length
    BEGIN OPEN sysMailSystem;
    Time.AppendCurrent[textualDateAndTime];
    String.AppendLongNumber[textualMessageLength, messageLength, 10];
  -- deliver message
    FOR localMailboxList ← localMailboxListHead, localMailboxList.nextMailbox
        UNTIL localMailboxList=NIL DO OPEN localMailboxList;
      IF located AND ~delivered THEN
        BEGIN ENABLE FTPError => CONTINUE;
        DeliverMessageToMailbox[sysMailSystem, textualDateAndTime, textualMessageLength, location];
        delivered ← TRUE;
        END;
      ENDLOOP;
    END; -- open
  END;

ForwardMessage: PROCEDURE [mailSystem: MailSystem, remoteMailboxList: Mailbox] =
  BEGIN
  -- Note:  Assumes message text staged;
  --   skips mailboxes showing delivered=TRUE;
  --   suppresses errors raised in individual forwarding attempts.
  -- not implemented
  END;

RetrieveMessages: PROCEDURE [mailSystem: MailSystem, localMailbox: Mailbox, processMessage: PROCEDURE [MessageInfo], sendBlock: PROCEDURE [UNSPECIFIED, POINTER, CARDINAL], sendBlockData: UNSPECIFIED] =
  BEGIN OPEN localMailbox;
  -- Note:  Assumes localMailbox is of length 1 showing located=TRUE;
  --   invocation of processMessage with byteCount=0 signifies end of file and
  --   precedes flushing of mailbox; assumes date and time and message lengths
  --   are free of the delimiters:
  --     dateAndTimeTerminator and messageLengthTerminator.
  -- ReadMessages procedure
    ReadMessages: PROCEDURE =
      BEGIN OPEN sysMailSystem;
      -- SendBlock procedure
        SendBlock: PROCEDURE [unused: UNSPECIFIED, source: POINTER, byteCount: CARDINAL] =
          BEGIN
          -- local variables
            bytePointerObject: BytePointerObject ← [source, FALSE, byteCount];
            character: CHARACTER;
            wordObject: WordObject;
          -- check for premature end-of-file
            IF byteCount = 0 AND (string # textualDateAndTime OR string.length # 0
            OR messageInfoObject.byteCount # 0) THEN Abort[unexpectedEndOfFile];
          -- consume source data
            UNTIL bytePointerObject.count = 0 DO
              SELECT TRUE FROM
                (messageInfoObject.byteCount = 0) =>
                  SELECT character ← LOOPHOLE[LoadByte[@bytePointerObject]] FROM
                    dateAndTimeTerminator => string ← textualMessageLength;
                    messageLengthTerminator =>
                      BEGIN
                      messageInfoObject.byteCount ←
                        String.StringToLongNumber[textualMessageLength, 10];
                      processMessage[@messageInfoObject];
                      textualDateAndTime.length ← textualMessageLength.length ← 0;
                      string ← textualDateAndTime;
                      END;
                    ENDCASE => String.AppendChar[string, character];
                (messageInfoObject.byteCount > 0 AND bytePointerObject.offset) =>
                  BEGIN
                  wordObject.lhByte ← LoadByte[@bytePointerObject];
                  sendBlock[sendBlockData, @wordObject, 1];
                  messageInfoObject.byteCount ← messageInfoObject.byteCount - 1;
                  END;
                ENDCASE =>
                  BEGIN
                  byteCount ← MIN[bytePointerObject.count,
                    IF messageInfoObject.byteCount > LAST[CARDINAL]
                      THEN LAST[CARDINAL]
                      ELSE Inline.LowHalf[messageInfoObject.byteCount]];
                  sendBlock[sendBlockData, bytePointerObject.address, byteCount];
                  AdvanceBytePointer[@bytePointerObject, byteCount];
                  messageInfoObject.byteCount ← messageInfoObject.byteCount - byteCount;
                  END;
              ENDLOOP;
          END;
      -- local constants
        textualDateAndTime: STRING = [maxStringLength];
        textualMessageLength: STRING = [maxStringLength];
      -- local variables
        fileHandle: FileHandle ← NIL;
        string: STRING ← textualDateAndTime;
        messageInfoObject: MessageInfoObject ← [
          byteCount: 0, deliveryDate: textualDateAndTime,
          opened: FALSE, deleted: FALSE];
      -- open mailbox file for readThenWrite
        [fileHandle, ] ← filePrimitives.OpenFile[fileSystem, location, readThenWrite, FALSE, NIL
            ! FTPError => IF ftpError = noSuchFile THEN CONTINUE];
        IF fileHandle = NIL THEN
          BEGIN
          processMessage[@messageInfoObject];
          RETURN;
          END;
        BEGIN ENABLE UNWIND => filePrimitives.CloseFile[fileSystem, fileHandle, FALSE];
      -- read messages:  date and time, message length, and text
        filePrimitives.ReadFile[fileSystem, fileHandle, SendBlock, NIL];
      -- signal end of file
        processMessage[@messageInfoObject];
      -- close mailbox file
      -- Note:  aborted=TRUE, requesting deletion of file.
        END; -- enable
        filePrimitives.CloseFile[fileSystem, fileHandle, TRUE];
      END;
  -- local constants
    sysMailSystem: SysMailSystem = LOOPHOLE[mailSystem];
  -- retrieve messages
    ManipulateMailbox[location, ReadMessages];
  END;

-- **********************!  Mailbox Primitives  !***********************

EnumerateMailboxes: PROCEDURE [sysMailSystem: SysMailSystem, processMailbox: PROCEDURE [STRING, STRING]] =
  BEGIN OPEN sysMailSystem;
  -- Note:  Assumes mailbox names and filenames are free of the delimiters:
  --   mailboxTerminator and fileTerminator.
  -- SendBlock procedure
    SendBlock: PROCEDURE [unused: UNSPECIFIED, source: POINTER, byteCount: CARDINAL] =
      BEGIN
      -- local variables
        bytePointerObject: BytePointerObject ← [source, FALSE, byteCount];
        character: CHARACTER;
      -- check for premature end of file
        IF byteCount = 0 AND (string = file OR string.length # 0) THEN
          Abort[unexpectedEndOfFile];
      -- consume source data
        UNTIL bytePointerObject.count = 0 DO
          SELECT character ← LOOPHOLE[LoadByte[@bytePointerObject]] FROM
            mailboxTerminator => string ← file;
            fileTerminator =>
              BEGIN
              processMailbox[mailbox, file];
              mailbox.length ← file.length ← 0;
              string ← mailbox;
              END;
            ENDCASE => String.AppendChar[string, character];
          ENDLOOP; 
      END;
  -- local constants
    mailbox: STRING = [maxStringLength];
    file: STRING = [maxStringLength];
  -- local variables
    string: STRING ← mailbox;
  -- enumerate mailboxes
    filePrimitives.ReadFile[fileSystem, directoryFileHandle, SendBlock, NIL];
  END;

DeliverMessageToMailbox: PROCEDURE [sysMailSystem: SysMailSystem, textualDateAndTime, textualMessageLength, file: STRING] =
  BEGIN OPEN sysMailSystem;
  -- Note:  Assumes text of message in staging file;
  --   assumes date and time and message length are free of the delimiters:
  --     dateAndTimeTerminator and messageLengthTerminator.
  -- WriteMessage procedure
    WriteMessage: PROCEDURE =
      BEGIN
      -- ReceiveBlock Procedure
        ReceiveBlock: PROCEDURE [unused: UNSPECIFIED, destination: POINTER,
            maxWordCount: CARDINAL] RETURNS [actualByteCount: CARDINAL] =
          BEGIN
          -- local variables
            bytePointerObject: BytePointerObject ←
              [destination, FALSE, bytesPerWord*maxWordCount];
            character: CHARACTER;
          -- produce destination data
            UNTIL bytePointerObject.count = 0 OR endOfHeader DO
              SELECT TRUE FROM
                (index < string.length) =>
                  BEGIN
                  character ← string[index];
                  index ← index + 1;
                  END;
                (string = textualDateAndTime) =>
                  BEGIN
                  character ← dateAndTimeTerminator;
                  string ← textualMessageLength;
                  index ← 0;
                  END;
                ENDCASE =>
                  BEGIN
                  character ← messageLengthTerminator;
                  endOfHeader ← TRUE;
                  END;
              StoreByte[@bytePointerObject, LOOPHOLE[character]];
              ENDLOOP;
          -- compute actual byte count for caller
            actualByteCount ← bytesPerWord*maxWordCount - bytePointerObject.count;
          END;
      -- local variables
        fileHandle: FileHandle;
        string: STRING ← textualDateAndTime;
        index: CARDINAL ← 0;
        endOfHeader: BOOLEAN ← FALSE;
      -- open file for append
        [fileHandle, ] ← filePrimitives.OpenFile[fileSystem, file, append, FALSE, NIL];
        BEGIN ENABLE UNWIND => filePrimitives.CloseFile[fileSystem, fileHandle, TRUE];
      -- write message:  date and time, message length, and text
        filePrimitives.WriteFile[fileSystem, fileHandle, ReceiveBlock, NIL];
        ForkTransferPair[fileSystem, filePrimitives.ReadFile, stagingFileHandle, filePrimitives.WriteFile, fileHandle];
      -- close file
        END; -- enable
        filePrimitives.CloseFile[fileSystem, fileHandle, FALSE];
      END;
  -- deliver message to mailbox
    ManipulateMailbox[file, WriteMessage];
  END;

ManipulateMailbox: PROCEDURE [file: STRING, procedure: PROCEDURE] =
  BEGIN
  -- local variables
    mailboxQueueElement: Element ← NIL;
  -- enqueue mailbox element
    mailboxQueueElement ← EnQueue[@mailboxQueueObject, NIL, file];
    BEGIN ENABLE UNWIND =>
      BEGIN
      IF mailboxQueueElement.locked THEN
        UnlockQueue[@mailboxQueueObject, mailboxQueueElement];
      IF mailboxQueueElement # NIL THEN
        DeQueue[@mailboxQueueObject, mailboxQueueElement];
      END;
  -- lock file
    LockQueue[@mailboxQueueObject, mailboxQueueElement, TRUE];
  -- dispatch caller
    procedure[];
  -- unlock file
    UnlockQueue[@mailboxQueueObject, mailboxQueueElement];
  -- dequeue mailbox element
    END; -- enable
    DeQueue[@mailboxQueueObject, mailboxQueueElement];
  END;

StoreByte: PUBLIC PROCEDURE [dstBytePointer: BytePointer, byte: Byte] =
  BEGIN
  -- Note:  Doesn't check for byte pointer exhaustion.
  -- local constants
    dBP: BytePointer = dstBytePointer;
    dWord: Word = dBP.address;
  -- store byte
    IF dBP.offset THEN dWord.rhByte ← byte ELSE dWord.lhByte ← byte;
  -- advance address and offset
    IF ~(dBP.offset ← ~dBP.offset) THEN dBP.address ← dBP.address + 1;
  -- decrement byte count
    dBP.count ← dBP.count - 1;
  END;

LoadByte: PUBLIC PROCEDURE [srcBytePointer: BytePointer] RETURNS [byte: Byte] =
  BEGIN
  -- Note:  Doesn't check for byte pointer exhaustion.
  -- local constants
    sBP: BytePointer = srcBytePointer;
    sWord: Word = sBP.address;
  -- load byte
    byte ← IF sBP.offset THEN sWord.rhByte ELSE sWord.lhByte;
  -- advance address and offset
    IF ~(sBP.offset ← ~sBP.offset) THEN sBP.address ← sBP.address + 1;
  -- decrement byte count
    sBP.count ← sBP.count - 1;
  END;
-- **********************!  Main Program  !***********************

-- no operation    

END. -- of FTPSysMail