-- 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