File: PeanutRetrieveImpl.mesa April 1, 1983 2:17 pm
Last edited by Bill Paxton, May 12, 1983 4:11 pm
Last edited by: Pausch, July 14, 1983 3:26 pm
Last edited by Doug Wyatt, December 7, 1983 3:16 pm
DIRECTORY
Atom USING [GetPropFromList, PutPropOnList], -- needed by ViewerOps.AddProp, FetchProp
BasicTime USING [FromPupTime, GMT, Now, Unpack, Unpacked], -- Now is needed by IO.time
Convert,
FS USING [Error, GetName, nullOpenFile, Open, OpenFile],
GVBasics,
GVRetrieve,
Icons,
IO USING [GetBlock, int, PutFR, rope, STREAM, time],
MessageWindow,
PeanutProfile USING [activeMailFile, automaticNewMail],
PeanutRetrieve,
PeanutSendMail,
PeanutWindow,
PrincOpsUtils, -- needed by INLINEs in Process
Process,
PutGet,
Rope USING [Cat, Concat, Equal, Fetch, Find, FromChar, FromRefText, ROPE, Size, Substr],
TEditDisplay,
TEditDocument,
TextEdit,
TextNode,
TiogaFileOps,
TiogaMenuOps,
TiogaOps,
UserCredentials USING [Get],
ViewerClasses USING [Viewer],
ViewerEvents USING [RegisterEventProc, ViewerEvent],
ViewerOps USING [AddProp, EnumerateViewers, EnumProc, FetchProp, OpenIcon, PaintViewer, SaveViewer, SetNewVersion],
ViewerTools USING [MakeNewTextViewer];
PeanutRetrieveImpl: CEDAR MONITOR
IMPORTS Atom, BasicTime, Convert, FS, GVRetrieve, IO, MessageWindow, PeanutProfile, PeanutSendMail, PeanutWindow, PrincOpsUtils, Process, PutGet, Rope, TEditDisplay, TextEdit, TextNode, TiogaFileOps, TiogaMenuOps, TiogaOps, UserCredentials, ViewerEvents, ViewerOps, ViewerTools
EXPORTS PeanutRetrieve
= BEGIN
************************************************************************
ROPE: TYPE = Rope.ROPE;
Viewer: TYPE = ViewerClasses.Viewer;
RName: TYPE = GVBasics.RName;
TiogaCTRL: GVBasics.ItemType = LOOPHOLE[1013B]; -- an item containing tioga formatting
ReportRope: PROCEDURE [r: ROPE] = { PeanutWindow.OutputRope[r] };
GetNewMsgs:
PUBLIC
ENTRY
PROC [open:
BOOL ←
TRUE] =
{ ENABLE UNWIND => NULL; [] ← InternalGetNewMsgs[open]; };
peanutProp: ATOM = $PeanutMailFileName;
SetMailFileName:
PROC[v: Viewer, name:
ROPE] = {
ViewerOps.AddProp[viewer: v, prop: peanutProp, val: name];
};
GetMailFileName:
PROC[v: Viewer]
RETURNS[
ROPE] = {
WITH ViewerOps.FetchProp[viewer: v, prop: peanutProp]
SELECT
FROM
rope: ROPE => RETURN[rope];
ENDCASE => RETURN[NIL];
};
FindMailViewer:
PUBLIC
PROC[name:
ROPE]
RETURNS[viewer: Viewer ←
NIL] = {
Enumerate top level viewers, looking for one with a matching $PeanutMailFile property
Test: ViewerOps.EnumProc
-- PROC[v: Viewer] RETURNS[continue: BOOL ← TRUE] -- = {
rope: ROPE = GetMailFileName[v];
IF rope#NIL AND Rope.Equal[rope, name] THEN { viewer ← v; RETURN[FALSE] };
};
ViewerOps.EnumerateViewers[Test];
};
GetMailViewer:
PUBLIC
ENTRY PROC[name:
ROPE]
RETURNS [mailViewer: Viewer] = {
Make this an entry so don't have two Get's at once for same mail file
ENABLE UNWIND => NULL;
mailViewer ← InternalGetMailViewer[name];
};
InternalGetMailViewer:
INTERNAL PROC[name:
ROPE]
RETURNS [mailViewer: Viewer] = {
mailViewer ← FindMailViewer[name];
IF mailViewer=
NIL
THEN {
file: ROPE ← Rope.Concat[name, ".mail"];
fh: FS.OpenFile ← FS.nullOpenFile;
mailDoc: TextNode.Ref ← NIL;
fh ← FS.Open[file ! FS.Error => IF error.group=user THEN CONTINUE];
IF fh#
FS.nullOpenFile
THEN {
If the Open succeeded, note the file's full name and read its contents.
file ← FS.GetName[fh].fullFName;
mailDoc ← PutGet.FromFileC[fh];
}
ELSE {
Create a document with a single node containing the file name.
Make the document's style Mail.style.
styleProp: ROPE = "(mail) style";
mailDoc ← TextEdit.DocFromNode[TextEdit.FromRope[file]];
TRUSTED { TiogaOps.PutProp[LOOPHOLE[mailDoc], $Prefix, styleProp] };
};
mailViewer ← ViewerTools.MakeNewTextViewer[info: [
name: file, file: file, data: mailDoc,
icon: PeanutWindow.messageSetIcon, iconic: TRUE
]];
SetMailFileName[mailViewer, name];
[] ← ViewerEvents.RegisterEventProc[proc: MessageSetHasBeenEdited,
event: edit, filter: mailViewer, before: TRUE];
[] ← ViewerEvents.RegisterEventProc[proc: MessageSetHasBeenSaved,
event: save, filter: mailViewer, before: FALSE];
TiogaMenuOps.DefaultMenus[mailViewer];
TiogaMenuOps.FirstLevelOnly[mailViewer];
};
};
InternalGetNewMsgs:
INTERNAL
PROC [open:
BOOL ←
TRUE]
RETURNS [numRetrieved:
INT] =
reads any new mail
{
allOK: BOOL← TRUE;
mailViewer: Viewer;
firstHeader: TiogaFileOps.Ref;
StashNewMessages:
PROC [retrieveOK:
BOOL]
RETURNS[doRemoteFlush:
BOOL] =
{
allOK ← allOK AND retrieveOK;
ViewerOps.SaveViewer[mailViewer]; -- doesn't return until the save is complete
don't do remote flush if ~retrieveOK
RETURN[retrieveOK AND flushRemoteMail];
};
mailViewer ← InternalGetMailViewer[PeanutProfile.activeMailFile];
[numRetrieved, firstHeader] ← AddNewMessages[StashNewMessages, mailViewer];
IF numRetrieved = 0
THEN {
ReportRope["\nNo messages were retrieved"]; RETURN};
IF mailViewer.iconic AND PeanutProfile.automaticNewMail THEN NULL
ELSE
TRUSTED {
tdd: TEditDocument.TEditDocumentData ← NARROW[mailViewer.data];
TEditDisplay.EstablishLine[tdd, [LOOPHOLE[firstHeader],0]];
ViewerOps.PaintViewer[mailViewer, client] };
IF ~allOK THEN ReportRope["\nSome messages may not have been retrieved"];
IF open AND mailViewer.iconic THEN ViewerOps.OpenIcon[mailViewer];
};
***********************************************************************
MessageSetHasBeenEdited:
PROC[viewer: Viewer,
event: ViewerEvents.ViewerEvent, before: BOOL] RETURNS[abort: BOOL ← FALSE] = {
IF before
THEN {
viewer.icon ← PeanutWindow.dirtyMessageSetIcon;
IF viewer.iconic THEN ViewerOps.PaintViewer[viewer: viewer, hint: all]; };
};
MessageSetHasBeenSaved:
PROC [viewer: Viewer,
event: ViewerEvents.ViewerEvent, before: BOOL] RETURNS[abort: BOOL ← FALSE] = {
IF
NOT before
THEN {
viewer.icon ← PeanutWindow.messageSetIcon;
IF viewer.iconic THEN ViewerOps.PaintViewer[viewer: viewer, hint: all]; };
};
CopyMessages:
PUBLIC
PROC [to:
ROPE, delete:
BOOL] = {
OPEN TiogaOps;
sourceViewer, destViewer: Viewer;
destDoc, sourceDoc, destLast, sourceLast, afterSource: Ref;
start, end: Location;
level: SelectionGrain;
caretBefore, pendingDelete: BOOL;
lockedPrimary, lockedSecondary, lockedDest, lockedSource: BOOL ← FALSE;
TopParent:
PROC [node, root: Ref]
RETURNS [parent: Ref] = {
DO
parent ← Parent[node];
IF parent=root THEN RETURN [node];
node ← parent;
ENDLOOP };
Cleanup:
PROC = {
IF lockedPrimary THEN UnlockSel[primary];
IF lockedSecondary THEN UnlockSel[secondary];
IF lockedDest THEN Unlock[destDoc];
IF lockedSource THEN Unlock[sourceDoc];
};
destViewer ← GetMailViewer[to];
LockSel[primary]; lockedPrimary ← TRUE;
[sourceViewer, start, end, level, caretBefore, pendingDelete] ← TiogaOps.GetSelection[];
IF sourceViewer=
NIL
OR sourceViewer.class.flavor#$Text
THEN {
UnlockSel[primary]; ReportRope["\nSelect message(s)."]; RETURN };
LockSel[secondary]; lockedSecondary ← TRUE;
destDoc ← ViewerDoc[destViewer];
Lock[destDoc]; lockedDest ← TRUE;
sourceDoc ← ViewerDoc[sourceViewer];
IF sourceDoc#destDoc THEN { Lock[sourceDoc]; lockedSource ← TRUE };
destLast ← LastChild[destDoc];
sourceLast ← TopParent[end.node, sourceDoc];
afterSource ← Next[sourceLast];
SelectBranches[
-- source
viewer: sourceViewer, level: branch, caretBefore: FALSE,
pendingDelete: delete, which: primary,
start: TopParent[start.node, sourceDoc], end: sourceLast];
SelectBranches[
-- destination
viewer: destViewer, start: destLast, end: destLast,
level: branch, caretBefore: FALSE, pendingDelete: FALSE, which: secondary];
ToSecondary[];
IF ~delete
THEN
-- restore original selection
SetSelection[sourceViewer, start, end, level, caretBefore, pendingDelete, primary]
ELSE IF afterSource # NIL THEN SelectPoint[sourceViewer, [afterSource,0], primary]
ELSE CancelSelection[primary];
ReportRope[IF delete THEN "\nMoved to " ELSE "\nCopied to "];
ReportRope[to];
Cleanup[];
};
***********************************************************************
msgPollingInterval: INT← 300; -- Number of seconds between mailbox polling.
flushRemoteMail: BOOLEAN← TRUE;
gvRetrieveHandle: GVRetrieve.Handle← NIL; -- cookie for receiving messages.
OpenConnection: PUBLIC PROC[user: RName] = {
This establishes a retrieve connection, and sets up a Mail Polling proc
CloseConnection[];
NewUser[user] ;
};
CloseConnection: PUBLIC PROC[] = {
This closes the connection, and invalidates the connection handle.
IF gvRetrieveHandle #
NIL
THEN{
GVRetrieve.Close[gvRetrieveHandle]; gvRetrieveHandle← NIL};
};
NewUser:
PUBLIC
PROC[user: RName] = {
Establish a new user on this connection.
IF gvRetrieveHandle =
NIL
THEN
gvRetrieveHandle ← GVRetrieve.Create[ msgPollingInterval, WatchMailBox ];
GVRetrieve.NewUser[gvRetrieveHandle, user, UserCredentials.Get[].password ] ;
} ;
lastStateReported: GVRetrieve.MBXState← unknown;
WatchMailBox:
PROC[newState: GVRetrieve.MBXState] = {
This is called when the condition of the mailbox changes
ActiveMailFileNotOpen:
PROC
RETURNS [
BOOL] = {
mailViewer: Viewer = FindMailViewer[PeanutProfile.activeMailFile];
RETURN [mailViewer=NIL OR mailViewer.iconic]
};
showTime: BOOL← TRUE;
status: ROPE;
IF newState = unknown THEN RETURN;
IF (lastStateReported = notEmpty)
AND (newState = someEmpty
OR newState = allEmpty)
THEN
{ status← NIL; PeanutWindow.SetNewMail[FALSE] }
ELSE SELECT newState
FROM
badName => {status← "\nYour user name is invalid, please log in"; showTime← FALSE};
badPwd => {status← "\nYour password is invalid"; showTime← FALSE};
cantAuth => {status← "\nCan't check your credentials at this time"; showTime← FALSE};
userOK => {status← "\nYour credentials are OK"; showTime← FALSE};
allDown => status← "\nAll of the mail servers are down";
someEmpty => status← "\nAll of the mail servers checked are empty";
allEmpty => {status← NIL; PeanutWindow.SetNewMail[FALSE]};
notEmpty => {status←
NIL; PeanutWindow.SetNewMail[
TRUE];
IF lastStateReported#notEmpty AND PeanutProfile.automaticNewMail
AND ActiveMailFileNotOpen[] THEN TRUSTED {Process.Detach[FORK ReadMail[]]}
};
ENDCASE => status← "\nBad State!";
lastStateReported ← newState;
IF status #
NIL
THEN
ReportRope[IF showTime THEN status.Concat[IO.PutFR[" at %g", IO.time[]]] ELSE status];
};
ReadMail:
ENTRY
PROC =
TRUSTED {
ENABLE UNWIND => NULL;
numRetrieved: INT;
Process.SetPriority[Process.priorityBackground];
IF (numRetrieved ← InternalGetNewMsgs[FALSE])=0 THEN RETURN;
MessageWindow.Append[IO.PutFR["New mail retrieved: %g", IO.time[]], TRUE];
MessageWindow.Blink[] };
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
MessageState: TYPE = { noMore, wasArchived, wasDeleted, OK, retrieveFailed } ;
AddNewMessages:
PUBLIC
PROC[
FinishedWithServer: PROC[BOOL] RETURNS[BOOL], mailViewer: Viewer]
RETURNS[numRetrieved: INT, firstHeader: TiogaFileOps.Ref] = {
This is the routine that actually reads the mail & makes the log entry.
It calls FinishedWithServer to commit the log before it flushes any particular mail server.
serverKnown: BOOLEAN ← FALSE;
mailDoc: TiogaOps.Ref ← TiogaOps.ViewerDoc[mailViewer];
IF gvRetrieveHandle =
NIL
THEN {
-- Open the connection if it's closed.
gvRetrieveHandle ← GVRetrieve.Create[ msgPollingInterval, WatchMailBox ] ;
GVRetrieve.NewUser[gvRetrieveHandle, PeanutSendMail.userRName,
UserCredentials.Get[].password];
} ;
SELECT gvRetrieveHandle.MailboxState[]
FROM
badName, badPwd => GOTO credentialsError;
cantAuth => GOTO noServers;
ENDCASE; --ok to try
numRetrieved← 0;
ReportRope[
IO.PutFR["\nCheck for new mail: %g",
IO.time[]]];
DO
-- Loops over servers.
m, formatting: ROPE;
messageState: MessageState;
timeStamp: GVBasics.Timestamp;
gvSender: RName;
messages: CARDINAL ← 0; -- the number of messages read from server.
archivedReported: BOOLEAN ← FALSE; -- seen an archived message?
Cycle through the servers, until you find one that has mail.
If it has mail, then go for it.
noMore: BOOLEAN; -- TRUE if no more servers.
serverState: GVRetrieve.ServerState; -- The state of the server.
headerNode: TiogaFileOps.Ref;
serverName: ROPE ;
TRUSTED {headerNode ← LOOPHOLE[TiogaOps.LastChild[mailDoc]]};
Step through the servers.
[noMore, serverState] ← gvRetrieveHandle.NextServer[];
IF noMore THEN EXIT; -- Last server? Then done.
serverKnown ← TRUE;
serverName ← gvRetrieveHandle.ServerName[];
ReportRope["\n"]; ReportRope[serverName]; ReportRope[": "];
IF serverState # notEmpty
THEN {
IF serverState = empty THEN ReportRope["empty"]
ELSE ReportRope["didn't respond"] ;
LOOP; -- Skip to the next server.
};
TiogaOps.Lock[mailDoc];
DO
ENABLE UNWIND => TiogaOps.Unlock[mailDoc];
[messageState, m, formatting, timeStamp, gvSender] ← ReadMessageRecord[] ;
SELECT messageState
FROM
noMore => EXIT ;
wasArchived =>
IF
NOT archivedReported
THEN
{ archivedReported ← TRUE; ReportRope["(archived messages exist)"]};
OK => NULL;
wasDeleted => NULL;
retrieveFailed => EXIT;
ENDCASE => ERROR;
IF
NOT (messageState = wasDeleted)
THEN {
DeleteTrailingCRs:
PROC [m:
ROPE]
RETURNS [
ROPE] = {
len: INT ← Rope.Size[m];
WHILE len > 0 AND Rope.Fetch[m, len-1] = '\n DO len ← len-1; ENDLOOP;
RETURN [Rope.Substr[m,0,len]] };
GetFieldContents:
PROC [name:
ROPE, maxLen:
INT]
RETURNS [contents:
ROPE] = {
start: INT ← Rope.Find[m, name];
IF start<0 THEN contents ← "?"
ELSE {
end: INT;
start ← start + Rope.Size[name];
end ← Rope.Find[m, "\n", start];
IF end <= start THEN contents ← "?"
ELSE
IF end > start+maxLen
THEN
contents ← Rope.Concat[Rope.Substr[m, start, maxLen-3], "..."]
ELSE contents ← Rope.Substr[m, start, end-start] }};
GetDateFromGMT:
PROC[time: BasicTime.
GMT]
RETURNS[
ROPE] = {
Returns a string of the form dd-mmm-yy (for example, 17-Nov-83)
unpacked: BasicTime.Unpacked = BasicTime.Unpack[time];
day: NAT = unpacked.day;
month: ROPE = Convert.RopeFromUnpackedTime[unpacked, months, months];
year: NAT = unpacked.year;
RETURN[
IO.PutFR["%2g-%g-%02g",
IO.int[day], IO.rope[month.Substr[len: 3]], IO.int[year MOD 100]]];
};
headerDate, headerName, headerSubject, header: ROPE;
messageNode: TiogaFileOps.Ref;
headerNode ← TiogaFileOps.InsertNode[headerNode, FALSE];
IF firstHeader=NIL THEN firstHeader ← headerNode;
TiogaFileOps.SetFormat[headerNode, "header"];
headerDate ← GetDateFromGMT[BasicTime.FromPupTime[timeStamp.time]];
IF Rope.Equal[gvSender, PeanutSendMail.userRName,
FALSE]
OR
Rope.Equal[gvSender, PeanutSendMail.simpleUserName,
FALSE]
THEN
headerName ← Rope.Concat["To: ", GetFieldContents["\nTo: ", 15]]
ELSE headerName ← gvSender;
headerSubject ← GetFieldContents["\nSubject: ", 45];
header ← Rope.Cat["\t", headerDate, "\t", headerName, "\t", headerSubject];
TiogaFileOps.SetContents[headerNode, header];
IF formatting=
NIL
THEN {
messageNode ← TiogaFileOps.InsertNode[headerNode, TRUE];
TiogaFileOps.SetContents[messageNode, DeleteTrailingCRs[m]] }
ELSE {
messageRoot, messageFirst, messageLast, headerN: TextNode.Ref;
messageRoot ← PutGet.FromRope[Rope.Cat[Rope.FromChar['\n],m,formatting]];
Restore leading CR. Get back a root node.
messageFirst ← TextNode.FirstChild[messageRoot];
TRUSTED { headerN ← LOOPHOLE[headerNode] };
headerN.child ← messageFirst;
messageLast ← TextNode.LastSibling[messageFirst];
messageLast.last ← TRUE; messageLast.next ← headerN;
messageRoot.child ←
NIL; messageRoot.props ←
NIL;
Clear out the links from the messageRoot to help garbage collection.
};
ViewerOps.SetNewVersion[mailViewer];
ReportRope["."];
messages ← messages + 1;
};
ENDLOOP ; -- Finished reading messages from this server.
TiogaOps.Unlock[mailDoc];
Flush the mailbox if desired, we've stashed the messages.
IF FinishedWithServer[messageState#retrieveFailed]
THEN
gvRetrieveHandle.Accept[ ! GVRetrieve.Failed =>
{ReportRope["\nFlush of remote messages failed; you may get these messages again"];
CONTINUE}];
IF messageState#retrieveFailed
THEN ReportRope[
IO.PutFR[": retrieved %g messages.", IO.int[messages] ]];
numRetrieved← numRetrieved + messages;
ENDLOOP ; -- End of servers loop, exit.
IF NOT serverKnown THEN GOTO noMailboxes;
EXITS
-- The error reporter for this routine.
noMailboxes => ReportRope[" No mail boxes"];
credentialsError => ReportRope[" Credentials error"];
noServers => ReportRope[" No servers responding"];
};
ReadMessageRecord:
PROC
RETURNS
[messageState: MessageState, m, formatting: ROPE,
timeStamp: GVBasics.Timestamp, gvSender: RName] = {
This routine reads the messages on this connection, returning messageState = noMore
when there aren't any more.
ENABLE GVRetrieve.Failed => {
ReportRope[
SELECT why
FROM
communicationFailure => "communication failure",
noSuchServer => "no such server",
connectionRejected => "server busy",
badCredentials => "bad credentials",
unknownFailure => "unknown Failure",
ENDCASE => "unknown Error" ];
GOTO gvFailed;
} ;
msgExists, archived, deleted: BOOLEAN;
item: GVBasics.ItemHeader;
blockSize: NAT = 500;
block: REF TEXT ← NIL;
m ← NIL;
[msgExists, archived, deleted] ← GVRetrieve.NextMessage[ gvRetrieveHandle ];
IF archived THEN messageState ← wasArchived ELSE messageState← OK;
IF deleted THEN { messageState ← wasDeleted; RETURN};
IF NOT msgExists THEN { messageState ← noMore; RETURN};
Now read all the items in the message, terminating on the LastItem, and
skipping the ones that we're not yet interested in.
[timeStamp, gvSender, ] ← GVRetrieve.StartMessage[ gvRetrieveHandle ] ;
DO
ReadItem:
PROC[h: GVRetrieve.Handle]
RETURNS[rope:
ROPE ←
NIL] = {
stream: IO.STREAM = GVRetrieve.GetItem[h];
IF block=NIL THEN block ← NEW[TEXT[blockSize]];
WHILE stream.GetBlock[block]>0
DO
rope ← rope.Concat[Rope.FromRefText[block]];
ENDLOOP;
};
item ← GVRetrieve.NextItem[gvRetrieveHandle];
SELECT item.type
FROM
PostMark => ERROR;
Sender => ERROR;
ReturnTo => ERROR;
Recipients => NULL;
Text => m ← ReadItem[gvRetrieveHandle];
TiogaCTRL => formatting ← ReadItem[gvRetrieveHandle];
Capability => NULL;
Audio => NULL;
updateItem => NULL;
reMail => NULL;
LastItem => EXIT;
ENDCASE => LOOP;
ENDLOOP;
EXITS
gvFailed => messageState← retrieveFailed;
} ;
END.