WalnutStreamImpl.mesa
Copyright © 1984 by Xerox Corporation. All rights reserved.
Willie-Sue, October 28, 1985 1:36:11 pm PST
Types and procedures dealing with Walnut log streams
Last Edited by: Wert, August 31, 1984 8:30:48 pm PDT
Last Edited by: Willie-Sue, January 8, 1985 1:18:28 pm PST
Last Edited by: Donahue, December 11, 1984 10:58:38 am PST
(Changed NextEntryID to return the message id of any entry that pertains to a message)
(Changing to use REF TEXT, do preallocation of log entry objects)
(Set Buffer options as suggested by Hagmann: 4 buffers @ 4 pages/buffer)
(Set Buffer options: 4 buffers @ 8 pages/buffer - getting too many aborts)
(Set Buffer options back to: 4 buffers @ 4 pages/buffer)
( Take out all references to version numbers)
DIRECTORY
AlpFile USING [LockOption, PropertyValuePair],
AlpineFS USING [ErrorFromStream, FileOptions, StreamOptions, Abort, Open, OpenFile, OpenFileFromStream, OpenOrCreate, StreamFromOpenFile, WriteProperties],
Atom USING [MakeAtomFromRefText],
BasicTime USING [GMT, nullGMT, FromPupTime, Now],
Convert USING [Error, IntFromRope, TimeFromRope],
FS USING [Error, nullOpenFile, OpenFile, StreamBufferParms, StreamOptions,
 Create, GetInfo, Open, PagesForBytes, SetByteCountAndCreatedTime,
 SetPageCount, StreamFromOpenFile, StreamOpen],
GVBasics USING [RName, Timestamp],
IO,
RefText USING[line, page, TrustTextAsRope],
Rope,
ViewerTools USING [TiogaContents],
WalnutKernelDefs USING [LogEntry, LogEntryObject, MsgLogEntry],
WalnutParseMsg USING [MsgHeaders, ParseProc, ParseMsgFromStream],
WalnutSendOps USING [RFC822Date, RopeFromStream],
WalnutStream USING [];
WalnutStreamImpl: CEDAR PROGRAM
IMPORTS
AlpineFS, Atom, BasicTime, Convert, FS, IO, RefText, Rope, WalnutParseMsg, WalnutSendOps
EXPORTS
WalnutStream
= BEGIN
OPEN WalnutStream;
Types
GMT: TYPE = BasicTime.GMT;
ROPE: TYPE = Rope.ROPE;
STREAM: TYPE = IO.STREAM;
LogEntry: TYPE = WalnutKernelDefs.LogEntry;
LogEntryObject: TYPE = WalnutKernelDefs.LogEntryObject;
MsgLogEntry: TYPE = WalnutKernelDefs.MsgLogEntry;
Variables
entryHeaderRope: ROPE = "*entry* %10g\n";
entryHeaderLen: INT = 19;
copyBuffer: REF TEXT = NEW[TEXT[RefText.page]];
field1: REF TEXTNEW[TEXT[RefText.line]];
field2: REF TEXTNEW[TEXT[RefText.line]];
field3: REF TEXTNEW[TEXT[RefText.line]];
field4: REF TEXTNEW[TEXT[RefText.line]];
-- the following appear in the other log files
logFileInfo: PUBLIC REF LogFileInfo LogEntryObject = NEW[LogFileInfo LogEntryObject];
createMsg: PUBLIC REF CreateMsg LogEntryObject = NEW[CreateMsg LogEntryObject];
expungeMsgs: PUBLIC REF ExpungeMsgs LogEntryObject =
  NEW[ExpungeMsgs LogEntryObject];
writeExpungeLog: PUBLIC REF WriteExpungeLog LogEntryObject =
  NEW[WriteExpungeLog LogEntryObject];
createMsgSet: PUBLIC REF CreateMsgSet LogEntryObject =
   NEW[CreateMsgSet LogEntryObject];
emptyMsgSet: PUBLIC REF EmptyMsgSet LogEntryObject =
  NEW[EmptyMsgSet LogEntryObject];
destroyMsgSet: PUBLIC REF DestroyMsgSet LogEntryObject =
  NEW[DestroyMsgSet LogEntryObject];
addMsg: PUBLIC REF AddMsg LogEntryObject = NEW[AddMsg LogEntryObject];
removeMsg: PUBLIC REF RemoveMsg LogEntryObject = NEW[RemoveMsg LogEntryObject];
moveMsg: PUBLIC REF MoveMsg LogEntryObject = NEW[MoveMsg LogEntryObject];
hasbeenRead: PUBLIC REF HasBeenRead LogEntryObject =
  NEW[HasBeenRead LogEntryObject];
recordNewMailInfo: PUBLIC REF RecordNewMailInfo LogEntryObject = NEW[RecordNewMailInfo LogEntryObject];
startCopyNewMail: PUBLIC REF StartCopyNewMail LogEntryObject =
  NEW[StartCopyNewMail LogEntryObject];
endCopyNewMailInfo: PUBLIC REF EndCopyNewMailInfo LogEntryObject =
  NEW[EndCopyNewMailInfo LogEntryObject];
acceptNewMail: PUBLIC REF AcceptNewMail LogEntryObject =
  NEW[AcceptNewMail LogEntryObject];
startReadArchiveFile: PUBLIC REF StartReadArchiveFile LogEntryObject =
  NEW[StartReadArchiveFile LogEntryObject];
endReadArchiveFile: PUBLIC REF EndReadArchiveFile LogEntryObject =
  NEW[EndReadArchiveFile LogEntryObject];
startCopyReadArchive: PUBLIC REF StartCopyReadArchive LogEntryObject =
  NEW[StartCopyReadArchive LogEntryObject];
endCopyReadArchiveInfo: PUBLIC REF EndCopyReadArchiveInfo LogEntryObject =
  NEW[EndCopyReadArchiveInfo LogEntryObject];
Procedures
Used for general opening of files
-- Opening streams
Open: PUBLIC PROC[name: ROPE, readOnly: BOOLFALSE, pages: INT ← 200,
  useOldIfFound: BOOLFALSE, exclusive: BOOLFALSE]
RETURNS [strm: STREAM] = {
IF name.Find[".alpine]", 0, FALSE] = -1 THEN {  -- file elsewhere
IF readOnly THEN
strm ← FS.StreamOpen[ fileName: name,
streamOptions: localStreamOptions, streamBufferParms: streamBufferOption]
ELSE {
openFile: FS.OpenFile ← FS.nullOpenFile;
IF useOldIfFound THEN
openFile ← FS.Open[name, $write ! FS.Error =>
IF error.code = $unknownFile THEN CONTINUE ELSE REJECT];
IF openFile = FS.nullOpenFile THEN
openFile ← FS.Create[name: name, keep: 2, pages: pages];
strm ← FS.StreamFromOpenFile[
openFile: openFile,
accessRights: $write,
streamOptions: localStreamOptions, streamBufferParms: streamBufferOption];
};
}
ELSE {  -- alpine file
openFile: AlpineFS.OpenFile;
IF readOnly THEN {
openFile ← AlpineFS.Open[name: name, options: alpineFileOptions];
strm ← AlpineFS.StreamFromOpenFile[openFile: openFile, streamOptions: alpineStreamOptions, streamBufferParms: streamBufferOption];
}
ELSE {
actualPages: INT;
lock: AlpFile.LockOption ← IF exclusive THEN [$write, $fail] ELSE [$none, $wait];
openFile ← AlpineFS.OpenOrCreate[  -- AlpineFS ignores the pages param
name: name, pages: pages, options: alpineFileOptions];
actualPages ← FS.GetInfo[openFile].pages;
IF pages > actualPages THEN FS.SetPageCount[openFile, pages];
strm ← AlpineFS.StreamFromOpenFile[openFile: openFile, accessRights: $write,
initialPosition: $start, streamOptions: alpineStreamOptions,
streamBufferParms: streamBufferOption];
};
};
};
alpineFileOptions: AlpineFS.FileOptions ← [
updateCreateTime: TRUE,
referencePattern: sequential,
recoveryOption: $log,
finishTransOnClose: TRUE
];
streamBufferOption: FS.StreamBufferParms = [vmPagesPerBuffer: 4, nBuffers: 4];
alpineStreamOptions: AlpineFS.StreamOptions ←
[ tiogaRead: FALSE,
commitAndReopenTransOnFlush: TRUE,
truncatePagesOnClose: FALSE,
finishTransOnClose: TRUE,
closeFSOpenFileOnClose: TRUE];
localStreamOptions: FS.StreamOptions ←
[ tiogaRead: FALSE,
commitAndReopenTransOnFlush: TRUE,
truncatePagesOnClose: FALSE,
finishTransOnClose: TRUE,
closeFSOpenFileOnClose: TRUE];
-- Miscellaneous stream operations
Aborted: PUBLIC PROC [strm: STREAM] RETURNS [aborted: BOOL] = {
code: ATOM ← AlpineFS.ErrorFromStream[strm].code;
aborted ← (code = $transAborted);
};
AbortStream: PUBLIC PROC[strm: STREAM] =
{ AlpineFS.Abort[AlpineFS.OpenFileFromStream[strm]] };
FlushStream: PUBLIC PROC[strm: STREAM, setCreateDate: BOOLFALSE] = {
IF setCreateDate THEN {
of: AlpineFS.OpenFile = AlpineFS.OpenFileFromStream[strm];
FS.SetByteCountAndCreatedTime[of, -1, BasicTime.Now[]];
};
strm.Flush[]
};
SetHighWaterMark: PUBLIC PROC[
strm: STREAM, hwmBytes: INT, numPages: INT, setCreateDate: BOOL] = {
of: AlpineFS.OpenFile = AlpineFS.OpenFileFromStream[strm];
prop: highWaterMark AlpFile.PropertyValuePair =
 [highWaterMark[FS.PagesForBytes[hwmBytes]]];
strm.SetLength[hwmBytes];
strm.Flush[];  -- make it notice the SetLength
IF setCreateDate THEN FS.SetByteCountAndCreatedTime[of, -1, BasicTime.Now[]];
AlpineFS.WriteProperties[of, LIST[prop]];
IF numPages = -1 THEN RETURN;
IF FS.GetInfo[of].pages <= numPages THEN RETURN;
FS.SetPageCount[of, numPages];
};
SetPosition: PUBLIC PROC[strm: STREAM, index: INT] RETURNS[ok: BOOL] = {
pos: INTIF index = -1 THEN strm.GetLength[] ELSE index;
ok ← TRUE;
strm.SetIndex[pos ! IO.Error, IO.EndOfStream => {ok ← FALSE; CONTINUE}];
};
ReadRope: PUBLIC PROC[strm: STREAM, len: INT] RETURNS[r: ROPE] = {
r ← WalnutSendOps.RopeFromStream[strm, strm.GetIndex[], len !
IO.EndOfStream => CONTINUE];
};
Reading and writing log entries
FindNextEntry: PUBLIC PROC[strm: STREAM] RETURNS[startPos: INT] = {
ENABLE IO.EndOfStream => GOTO exit;
state: INTEGER ← 0;
length: INT;
initialPos: INT = strm.GetIndex[];
startPos ← -1;
DO
SELECT strm.GetChar[] FROM
'* => IF state = 6 THEN state ← 7 ELSE state ← 1;
'e => IF state = 1 THEN state ← 2 ELSE state ← 0;
'n => IF state = 2 THEN state ← 3 ELSE state ← 0;
't => IF state = 3 THEN state ← 4 ELSE state ← 0;
'r => IF state = 4 THEN state ← 5 ELSE state ← 0;
'y => IF state = 5 THEN state ← 6 ELSE state ← 0;
ENDCASE => state ← 0;
IF state = 7 THEN {
strm.SetIndex[startPos ← strm.GetIndex[] - 7];
[, length] ← CheckForValidPrefix[strm];
IF length # -1 THEN {
strm.SetIndex[startPos];
RETURN
};
strm.SetIndex[startPos+1];
};
ENDLOOP;
EXITS
exit => {startPos ← -1; RETURN};
};
ReadEntry: PUBLIC PROC[strm: STREAM, quick: BOOL] RETURNS[le: LogEntry, length: INT] = {
ENABLE IO.EndOfStream => ERROR; -- Shouldn't get EOS's
type: ATOM;
startPos: INT;
[startPos, length] ← CheckForValidPrefix[strm];
IF length = -1 THEN RETURN;  -- not a valid entry here
type ← Atom.MakeAtomFromRefText[strm.GetLine[field1]];
SELECT type FROM
$LogFileInfo => {
logFileInfo.key ← strm.GetLine[field1];
logFileInfo.internalFileID ←
Convert.IntFromRope[RefText.TrustTextAsRope[strm.GetLine[field2]]];
logFileInfo.logSeqNo ←
Convert.IntFromRope[RefText.TrustTextAsRope[strm.GetLine[field2]]];
RETURN[logFileInfo, length];
};
$CreateMsg => {
createMsg.msg ← strm.GetLine[field1];
createMsg.textLen ← strm.GetInt[];
createMsg.formatLen ← strm.GetInt[];
[] ← strm.GetChar[];   -- glide over the CR after formatLen
createMsg.entryStart ← startPos;
createMsg.textOffset ← strm.GetIndex[] - startPos;
IF quick THEN ScanForHeadersLen[strm, createMsg]
ELSE MsgEntryInfoFromStream[strm, createMsg];
strm.SetIndex[startPos+length];  -- consume entire entry
RETURN[createMsg, length];
};
$ExpungeMsgs => RETURN[expungeMsgs, length];
$WriteExpungeLog => {
writeExpungeLog.internalFileID ←
Convert.IntFromRope[RefText.TrustTextAsRope[strm.GetLine[field1]]];
RETURN[writeExpungeLog, length];
};
$CreateMsgSet => {
createMsgSet.msgSet ← strm.GetLine[field1];
RETURN[createMsgSet, length];
};
$EmptyMsgSet => {
emptyMsgSet.msgSet ← strm.GetLine[field1];
RETURN[emptyMsgSet, length];
};
$DestroyMsgSet => {
destroyMsgSet.msgSet ← strm.GetLine[field1];
RETURN[destroyMsgSet, length];
};
$AddMsg => {
addMsg.msg ← strm.GetLine[field1];
addMsg.to ← strm.GetLine[field3];
RETURN[addMsg, length];
};
$RemoveMsg => {
removeMsg.msg ← strm.GetLine[field1];
removeMsg.from ← strm.GetLine[field2];
RETURN[removeMsg, length];
};
$MoveMsg => {
moveMsg.msg ← strm.GetLine[field1];
moveMsg.from ← strm.GetLine[field2];
moveMsg.to ← strm.GetLine[field3];
RETURN[moveMsg, length];
};
$HasBeenRead => {
hasbeenRead.msg ← strm.GetLine[field1];
RETURN[hasbeenRead, length];
};
$RecordNewMailInfo => {
recordNewMailInfo.logLen ←
Convert.IntFromRope[RefText.TrustTextAsRope[strm.GetLine[field1]]];
recordNewMailInfo.when ←
Convert.TimeFromRope[RefText.TrustTextAsRope[strm.GetLine[field2]]];
recordNewMailInfo.server ← strm.GetLine[field3];
recordNewMailInfo.num ←
Convert.IntFromRope[RefText.TrustTextAsRope[strm.GetLine[field4]]];
RETURN[recordNewMailInfo, length]
};
$StartCopyNewMail => RETURN[startCopyNewMail, length];
$EndCopyNewMailInfo => {
endCopyNewMailInfo.startCopyPos ←
Convert.IntFromRope[RefText.TrustTextAsRope[strm.GetLine[field1]]];
RETURN[endCopyNewMailInfo, length];
};
$AcceptNewMail => RETURN[acceptNewMail, length];
$StartReadArchiveFile => {
startReadArchiveFile.file ← strm.GetLine[field1];
startReadArchiveFile.msgSet ← strm.GetLine[field2];
RETURN[startReadArchiveFile, length];
};
$EndReadArchiveFile => RETURN[endReadArchiveFile, length];
$StartCopyReadArchive => RETURN[startCopyReadArchive, length];
$EndCopyReadArchiveInfo => {
endCopyReadArchiveInfo.startCopyPos ←
Convert.IntFromRope[RefText.TrustTextAsRope[strm.GetLine[field1]]];
RETURN[endCopyReadArchiveInfo, length];
};
ENDCASE => ERROR;
};
PeekEntry: PUBLIC PROC [strm: STREAM]
RETURNS
[ident: ATOM, msgID: REF TEXT, length: INT] = {
startPos: INT;
BEGIN ENABLE IO.EndOfStream => GOTO eos;
[startPos, length] ← CheckForValidPrefix[strm];
IF length = -1 THEN RETURN;  -- not a valid entry here
ident ← Atom.MakeAtomFromRefText[strm.GetLine[field1]];
SELECT ident FROM
$CreateMsg => msgID ← strm.GetLine[field1];
$AddMsg => msgID ← strm.GetLine[field1];
$RemoveMsg => msgID ← strm.GetLine[field1];
$MoveMsg => msgID ← strm.GetLine[field1];
$DestroyMsg => msgID ← strm.GetLine[field1];
$HasBeenRead => msgID ← strm.GetLine[field1];
ENDCASE => NULL;
EXITS
eos => length ← -1;  -- not a valid entry here
END;
strm.SetIndex[startPos];
};
WriteEntry: PUBLIC PROC[strm: STREAM, le: LogEntry, pos: INT ← -1]
RETURNS[startPos: INT] = {
entry: ROPE;
extra, length: INT ← 0;
TRUSTED { WITH le: le SELECT FROM
LogFileInfo =>
entry ← IO.PutFR["LogFileInfo\n%g\n%g\n%g\n",
IO.text[le.key],
IO.int[le.internalFileID],
IO.int[le.logSeqNo]
];
CreateMsg => {
entry ← IO.PutFR["CreateMsg\n%g\n%10g %10g\n",
IO.text[le.msg],
IO.int[le.textLen],
IO.int[le.formatLen]
];
extra ← le.textLen + le.formatLen + 1;
};
ExpungeMsgs => entry ← "ExpungeMsgs\n";
WriteExpungeLog =>
entry ← IO.PutFR["WriteExpungeLog\n%g\n",
IO.int[le.internalFileID]
];
CreateMsgSet =>
entry ← IO.PutFR["CreateMsgSet\n%g\n",
IO.text[le.msgSet]
];
EmptyMsgSet =>
entry ← IO.PutFR["EmptyMsgSet\n%g\n",
IO.text[le.msgSet]
];
DestroyMsgSet =>
entry ← IO.PutFR["DestroyMsgSet\n%g\n",
IO.text[le.msgSet]
];
AddMsg =>
entry ← IO.PutFR["AddMsg\n%g\n%g\n",
IO.text[le.msg],
IO.text[le.to]
];
RemoveMsg =>
entry ← IO.PutFR["RemoveMsg\n%g\n%g\n",
IO.text[le.msg],
IO.text[le.from]
];
MoveMsg =>
entry ← IO.PutFR["MoveMsg\n%g\n%g\n%g\n",
IO.text[le.msg],
IO.text[le.from],
IO.text[le.to]
];
HasBeenRead =>
entry ← IO.PutFR["HasBeenRead\n%g\n",
IO.text[le.msg],
];
RecordNewMailInfo =>
entry ← IO.PutFR["RecordNewMailInfo\n%g\n%g\n%g\n%g\n",
IO.int[le.logLen],
IO.time[le.when],
IO.text[le.server],
IO.int[le.num]
];
StartCopyNewMail => entry ← "StartCopyNewMail\n";
EndCopyNewMailInfo =>
entry ← IO.PutFR["EndCopyNewMailInfo\n%g\n",
IO.int[le.startCopyPos]
];
AcceptNewMail => entry ← "AcceptNewMail\n";
StartReadArchiveFile =>
entry ← IO.PutFR["StartReadArchiveFile\n%g\n%g\n",
IO.text[le.file],
IO.text[le.msgSet]
];
EndReadArchiveFile => entry ← "EndReadArchiveFile\n";
StartCopyReadArchive => entry ← "StartCopyReadArchive\n";
EndCopyReadArchiveInfo =>
entry ← IO.PutFR["EndCopyReadArchiveInfo\n%g\n",
IO.int[le.startCopyPos]
];
ENDCASE => ERROR;
};
entry: the rope representation of the log entry
write at the end of the stream unless given a pos (for fixing up CreateMsg headers)
startPos ← IF pos = -1 THEN strm.GetLength[] ELSE pos;
strm.SetIndex[startPos];
length ← entry.Length[] + extra + entryHeaderLen;  -- for messages
strm.PutRope[IO.PutFR[entryHeaderRope, IO.int[length]]];  -- entryHeaderLen (19 characters)
strm.PutRope[entry];
};
WriteMsgBody: PUBLIC PROC[strm: STREAM, body: ViewerTools.TiogaContents] = {
strm.PutRope[body.contents];
strm.PutRope[body.formatting];
strm.PutChar['\n];
};
Overwrite: PUBLIC PROC[to, from: STREAM, startPos: INT, fromPos: INT ← -1] = {
IF startPos = -1 THEN to.SetIndex[to.GetLength[]] ELSE to.SetIndex[startPos];
IF fromPos = -1 THEN from.SetIndex[0] ELSE from.SetIndex[fromPos];
StrmToStrmCopy[to, from];
};
CopyBytes: PUBLIC PROC[from, to: STREAM, num: INT] = {
bytes: INT ← num;
WHILE bytes >= 512 DO
[] ← from.GetBlock[copyBuffer, 0, 512];
to.PutBlock[copyBuffer];
bytes ← bytes - 512;
ENDLOOP;
IF bytes # 0 THEN {
[] ← from.GetBlock[copyBuffer, 0, bytes];
to.PutBlock[copyBuffer];
};
};
StrmToStrmCopy: PROC[to, from: STREAM] = {
DO
IF from.GetBlock[copyBuffer, 0, 512] = 0 THEN EXIT;
to.PutBlock[copyBuffer];
ENDLOOP
};
CheckForValidPrefix: PROC [strm: STREAM] RETURNS [startPos, length: INT] = {
entryRope: ROPE = "*entry* ";
lenRope, prefix: ROPE;
startPos ← strm.GetIndex[];
prefix ← WalnutSendOps.RopeFromStream[strm, startPos, entryHeaderLen];
IF NOT prefix.Find[entryRope] = 0 THEN {
strm.SetIndex[startPos];
RETURN[startPos, -1];
};
IF NOT prefix.Fetch[entryHeaderLen-1] = '\n THEN {
strm.SetIndex[startPos];
RETURN[startPos, -1];
};
lenRope ← prefix.Substr[entryRope.Length[], 10];
length ← Convert.IntFromRope[lenRope ! Convert.Error => {
length ← -1;
strm.SetIndex[startPos];
CONTINUE }];
};
MsgEntryInfoFromStream: PUBLIC PROC[strm: STREAM, mle: MsgLogEntry] = {
date: ROPE = "Date";
subject: ROPE = "Subject";
from: ROPE = "From";
sender: ROPE = "Sender";
to: ROPE = "To";
mh: WalnutParseMsg.MsgHeaders;
WantThisField: WalnutParseMsg.ParseProc = {
SELECT TRUE FROM
fieldName.Equal[date, FALSE] => RETURN[TRUE, TRUE];
fieldName.Equal[subject, FALSE] => RETURN[TRUE, TRUE];
fieldName.Equal[from, FALSE] => RETURN[TRUE, TRUE];
fieldName.Equal[sender, FALSE] => RETURN[TRUE, TRUE];
fieldName.Equal[to, FALSE] => RETURN[TRUE, TRUE];
ENDCASE => RETURN[FALSE, TRUE];
};
-- sigh, the joys of re-using the same MsgLogEntry; must clear all entries
mle.date ← BasicTime.nullGMT;
mle.subject ← NIL;
mle.sender ← NIL;
mle.to ← NIL;
IF strm.PeekChar[] = '\n THEN [] ← strm.GetChar[];   -- formatting madness
mh ← WalnutParseMsg.ParseMsgFromStream[strm, mle.textLen, WantThisField];
FOR mhL: WalnutParseMsg.MsgHeaders ← mh, mhL.rest UNTIL mhL=NIL DO
fieldName: ROPE = mhL.first.fieldName;
SELECT TRUE FROM
fieldName.Equal[date, FALSE] => mle.date ← Convert.TimeFromRope[mhL.first.value !
Convert.Error => {mle.date ← BasicTime.Now[]; CONTINUE } ];
fieldName.Equal[subject, FALSE] => mle.subject ← mhL.first.value;
fieldName.Equal[sender, FALSE] => mle.sender ← mhL.first.value;
fieldName.Equal[to, FALSE] => mle.to ← mhL.first.value;
fieldName.Equal[from, FALSE] =>
IF mle.sender = NIL THEN mle.sender ← mhL.first.value;
ENDCASE => NULL;
ENDLOOP;
};
ScanForHeadersLen: PROC[strm: STREAM, mle: MsgLogEntry] = {
lastWasCR: BOOLFALSE;
hLen: INT ← 0;
WHILE hLen <= mle.textLen DO
hLen ← hLen + 1;
IF strm.GetChar[] = '\n THEN {
IF lastWasCR THEN { mle.headersLen ← hLen; RETURN };
lastWasCR ← TRUE;
}
ELSE lastWasCR ← FALSE;
ENDLOOP;
mle.headersLen ← mle.textLen;  -- not found but don't cause error
};
ConstructMsgID: PUBLIC PROC[ts: GVBasics.Timestamp, gvSender: GVBasics.RName]
RETURNS[msgID: ROPE] = {
tr: ROPE ← WalnutSendOps.RFC822Date[BasicTime.FromPupTime[ts.time]];
IF gvSender.Fetch[0] = '" THEN {
pos: INT = gvSender.Find["\"", 1];
IF pos # -1 THEN
gvSender ←
Rope.Concat[Rope.Substr[gvSender, 1, pos - 1], Rope.Substr[gvSender, pos+1]];
};
msgID ← IO.PutFR["%g $ %b#%b@%g",
[rope[gvSender]], [integer[ts.net]], [integer[ts.host]], [rope[tr]]];
};
for debugging
GetFileLength: PROC[strm: STREAM] RETURNS[INT] =
{ RETURN[strm.GetLength[]] };
END.