DIRECTORY Basics, BasicTime, Commander, Convert, IO, LoganBerry, LoganBerryEntry, RedBlackTree, RefText, Rope, RuntimeError, SimpleFeedback, SunRPC, SunRPCBinding, TapFilter, TapMsgQueue, UserProfile, WalnutDefs, WalnutOps, TabPostOffice; WalnutPostOfficeServerImpl: CEDAR PROGRAM IMPORTS Basics, BasicTime, Commander, Convert, IO, LoganBerryEntry, RedBlackTree, RefText, Rope, RuntimeError, SimpleFeedback, SunRPCBinding, TabPostOffice, TapFilter, TapMsgQueue, UserProfile, WalnutDefs, WalnutOps ~ BEGIN ROPE: TYPE = Rope.ROPE; STREAM: TYPE = IO.STREAM; MsgInfo: TYPE = TabPostOffice.MsgInfo; server: SunRPC.Server; wH: WalnutOps.WalnutOpsHandle; noError: TabPostOffice.ErrorInfo = [errno: 0, msg: NIL]; noFilters: TabPostOffice.FilterSet = NEW[TabPostOffice.SeqType2Object[0]]; MsgEntry: TYPE = REF MsgEntryRecord; MsgEntryRecord: TYPE = RECORD [ walnutID: ROPE, info: MsgInfo, text: ROPE _ NIL, bodyStart: INT _ -1 ]; SessionInfo: TYPE = RECORD [ whenStarted: BasicTime.GMT, selectList: TabPostOffice.PropInfoArray, orderList: TabPostOffice.PropertyArray, msgs: RedBlackTree.Table ]; session: SessionInfo; StartSession: TabPostOffice.tpostartsessionProc ~ { ENABLE WalnutDefs.Error => {PrintDebug["WALNUT ERROR: who=%s, code=%s, explanation=%s", IO.atom[who], IO.atom[code], IO.rope[explanation]]; CONTINUE}; pThreshold: INT ฌ -1; PrintDebug["BEGIN StartSession: numselects=%s, plist=%s, numorders=%s, olist=%s", IO.int[numselects], IO.rope[FmtPropI[plist]], IO.int[numorders], IO.rope[FmtPropA[olist]]]; session.whenStarted ฌ BasicTime.Now[]; session.selectList ฌ plist; session.orderList ฌ olist; session.msgs ฌ RedBlackTree.Create[getKey: GetKey, compare: CompareProc]; res.total ฌ 0; res.unread ฌ 0; res.new ฌ 0; IF plist = NIL OR plist.size = 0 THEN { -- use priority threshold by default pThreshold ฌ UserProfile.Number[key: "WalnutPostOffice.PriorityThreshold", default: 50]; }; wH ฌ WalnutOps.GetHandleForRootfile[UserProfile.Token["Walnut.WalnutRootFile"]]; FOR msgSets: LIST OF ROPE _ SelectedMsgSets[plist], msgSets.rest WHILE msgSets # NIL DO enum: WalnutOps.EnumeratorForMsgs; msg: ROPE; PrintDebug["Enumerating msgSet=%s", IO.rope[msgSets.first]]; enum ฌ WalnutOps.EnumerateMsgsInMsgSet[wH, msgSets.first ! WalnutDefs.Error => {PrintDebug["WALNUT ERROR: who=%s, code=%s, explanation=%s", IO.atom[who], IO.atom[code], IO.rope[explanation]]; CONTINUE}]; msg ฌ WalnutOps.NextMsg[enum].msgID; WHILE msg # NIL DO IF MatchesPriority[msg, pThreshold] THEN { msgEntry: MsgEntry ฌ GetMsgEntry[msg]; IF MatchesSelect[msgEntry, plist] THEN { RedBlackTree.Insert[session.msgs, msgEntry, msgEntry! RedBlackTree.DuplicateKey => {PrintDebug["DuplicateKey!"]; CONTINUE}]; IF msgEntry.info.status # $Read THEN res.unread ฌ res.unread + 1; }; }; msg ฌ WalnutOps.NextMsg[enum].msgID; ENDLOOP; ENDLOOP; res.total ฌ AssignMsgIds[session]; res.new ฌ res.unread; -- technically this isn't right, but it's ok for now [res.loPri, res.hiPri] ฌ GetPriorityRange[session]; res.lastModifyTime ฌ 0; res.lastAccessTime ฌ LAST[INT32]; res.e ฌ noError; PrintDebug["END StartSession"]; }; GetMsgInfo: TabPostOffice.tpogetmsginfoProc ~ { PrintDebug["BEGIN GetMsgInfo: beginId=%s, endId=%s", IO.int[beginId], IO.int[endId]]; res.info ฌ NEW[TabPostOffice.SeqType4Object[endId-beginId+1]]; FOR id: INT IN [beginId..endId] DO msgEntry: MsgEntry ฌ LookupById[session, id]; res.info[msgEntry.info.msgId-beginId] ฌ msgEntry.info; ENDLOOP; res.e ฌ noError; PrintDebug["END GetMsgInfo"]; }; GetMsgText: TabPostOffice.tpogetmsgtextProc ~ { msgEntry: MsgEntry; PrintDebug["BEGIN GetMsgText: msgId=%s, beginByte=%s, numBytes=%s, whence=%s", IO.int[msgId], IO.int[beginByte], IO.int[numBytes], IO.rope[FmtWhence[whence]]]; msgEntry ฌ LookupById[session, msgId]; res.body ฌ msgEntry.text; IF whence = $BOB THEN res.body ฌ GetBody[res.body]; res.e ฌ noError; PrintDebug["END GetMsgText"]; }; CheckNewMail: TabPostOffice.tpochecknewmailProc ~ { PrintDebug["BEGIN CheckNewMail"]; res ฌ [newMail: FALSE, e: noError]; PrintDebug["END CheckNewMail"]; }; partialFolders: BOOLEAN ฌ TRUE; GetFolders: TabPostOffice.tpogetfoldersProc ~ { ENABLE WalnutDefs.Error => {PrintDebug["WALNUT ERROR: who=%s, code=%s, explanation=%s", IO.atom[who], IO.atom[code], IO.rope[explanation]]; CONTINUE}; list: LIST OF ROPE; num: CARD ฌ 0; PrintDebug["BEGIN GetFolders"]; IF partialFolders THEN list ฌ LIST["Active", "CACM", "Cedar10", "LoganBerry", "Modula3", "ParcPad", "Tapestry", "Wallaby"] ELSE list ฌ WalnutOps.MsgSetNames[wH].mL; FOR mL: LIST OF ROPE _ list, mL.rest WHILE mL#NIL DO num ฌ num+1; ENDLOOP; res.numFolders ฌ num; res.folders ฌ NEW[TabPostOffice.SeqType3Object[num]]; num ฌ 0; FOR mL: LIST OF ROPE _ list, mL.rest WHILE mL#NIL DO res.folders[num] ฌ mL.first; num ฌ num+1; ENDLOOP; res.e ฌ noError; PrintDebug["END GetFolders"]; }; MarkMsg: TabPostOffice.tpomarkmsgProc ~ { PrintDebug["BEGIN MarkMsg: msgId=%s, markID=%s, markMsg=%s", IO.int[msgId], IO.rope[markID], IO.rope[markMsg]]; res ฌ noError; PrintDebug["END MarkMsg"]; }; EndSession: TabPostOffice.tpoendsessionProc ~ { PrintDebug["BEGIN EndSession"]; res ฌ noError; PrintDebug["END EndSession"]; }; defaultMsgSet: ROPE = "Active"; SelectedMsgSets: PROC [selectList: TabPostOffice.PropInfoArray] RETURNS [msgsets: LIST OF ROPE] ~ { msgsets ฌ NIL; FOR i: INT IN [0..selectList.size) DO IF Rope.Equal[selectList[i].p, "Folder", FALSE] THEN { msgSet: ROPE; IF NOT Rope.Equal[selectList[i].r, "Equals", FALSE] THEN LOOP; -- not supported SELECT TRUE FROM Rope.Equal[selectList[i].v, "All", FALSE] => msgSet ฌ "ZZZAll"; Rope.Equal[selectList[i].v, "AllMail", FALSE] => msgSet ฌ "ZZZAll"; Rope.Equal[selectList[i].v, "NewMail", FALSE] => msgSet ฌ "ZZZNew"; Rope.Equal[selectList[i].v, "New", FALSE] => msgSet ฌ "ZZZNew"; ENDCASE => msgSet ฌ selectList[i].v; msgsets ฌ CONS[msgSet, msgsets]; }; ENDLOOP; IF msgsets = NIL THEN { msgsets ฌ UserProfile.ListOfTokens[key: "WalnutPostOffice.ActiveMessageSets", default: LIST[defaultMsgSet]]; }; }; MatchesPriority: PROC [walnutID: ROPE, threshold: INT] RETURNS [matches: BOOLEAN] ~ { priority: INT; IF threshold < 0 THEN RETURN[TRUE]; priority ฌ MsgInterestLevel[walnutID]; matches ฌ priority >= threshold; RETURN[matches]; }; MatchesSelect: PROC [msgEntry: MsgEntry, selectList: TabPostOffice.PropInfoArray] RETURNS [matches: BOOLEAN] ~ { matches ฌ TRUE; IF msgEntry = NIL THEN RETURN[FALSE]; FOR i: INT IN [0..selectList.size) DO IF Rope.Equal[selectList[i].p, "Priority", FALSE] THEN { threshold: INT ฌ -1; threshold ฌ Convert.IntFromRope[selectList[i].v ! Convert.Error => CONTINUE]; IF threshold = -1 THEN LOOP; -- bogus value SELECT TRUE FROM Rope.Equal[selectList[i].r, "GreaterThan", FALSE] => matches ฌ msgEntry.info.priority > threshold; Rope.Equal[selectList[i].r, "GreaterThanEquals", FALSE] => matches ฌ msgEntry.info.priority >= threshold; Rope.Equal[selectList[i].r, "Equals", FALSE] => matches ฌ msgEntry.info.priority = threshold; Rope.Equal[selectList[i].r, "LessThanEquals", FALSE] => matches ฌ msgEntry.info.priority <= threshold; Rope.Equal[selectList[i].r, "LessThan", FALSE] => matches ฌ msgEntry.info.priority < threshold; ENDCASE => NULL; }; IF matches = FALSE THEN EXIT; ENDLOOP; RETURN[matches]; }; GetKey: RedBlackTree.GetKey ~ { -- can't be an internal procedure in Cedar10.1 RETURN[data]; }; CompareProc: RedBlackTree.Compare ~ { result: Basics.Comparison; key1: MsgEntry ฌ NARROW[k]; key2: MsgEntry ฌ NARROW[data]; IF key1.info.msgId # -1 AND key2.info.msgId # -1 THEN result ฌ Basics.CompareInt[key1.info.msgId, key2.info.msgId] ELSE result ฌ CompareList[key1, key2, session.orderList]; RETURN[result]; }; CompareList: PROC [m1, m2: MsgEntry, orderList: TabPostOffice.PropertyArray] RETURNS [Basics.Comparison] ~ { result: Basics.Comparison ฌ $equal; FOR i: INT IN [0..orderList.size) DO SELECT TRUE FROM Rope.Equal[orderList[i], "Priority", FALSE] => result ฌ ComparePriority[m1.info.priority, m2.info.priority]; Rope.Equal[orderList[i], "Date", FALSE] => result ฌ CompareDate[m1.info.date, m2.info.date]; Rope.Equal[orderList[i], "From", FALSE] => result ฌ Rope.Compare[m1.info.from, m2.info.from, FALSE]; Rope.Equal[orderList[i], "Subject", FALSE] => result ฌ Rope.Compare[m1.info.subject, m2.info.subject, FALSE]; Rope.Equal[orderList[i], "To", FALSE] => result ฌ Rope.Compare[m1.info.to, m2.info.to, FALSE]; Rope.Equal[orderList[i], "New", FALSE] => result ฌ CompareStatus[m1.info.status, m2.info.status, $New, TRUE]; Rope.Equal[orderList[i], "Old", FALSE] => result ฌ CompareStatus[m1.info.status, m2.info.status, $New, FALSE]; Rope.Equal[orderList[i], "Read", FALSE] => result ฌ CompareStatus[m1.info.status, m2.info.status, $Read, TRUE]; Rope.Equal[orderList[i], "Unread", FALSE] => result ฌ CompareStatus[m1.info.status, m2.info.status, $Read, FALSE]; ENDCASE => result ฌ $equal; IF result # $equal THEN EXIT; ENDLOOP; IF result = equal THEN result ฌ Rope.Compare[m1.walnutID, m2.walnutID]; RETURN [result]; }; ComparePriority: PROC [p1, p2: INT] RETURNS [Basics.Comparison] ~ { RETURN [Basics.CompareInt[p2, p1]]; }; CompareDate: PROC [d1, d2: ROPE, oldestFirst: BOOLEAN ฌ TRUE] RETURNS [Basics.Comparison] ~ { result: Basics.Comparison; t1, t2: BasicTime.GMT; t1 ฌ Convert.TimeFromRope[d1 ! Convert.Error => CONTINUE]; t2 ฌ Convert.TimeFromRope[d2 ! Convert.Error => CONTINUE]; SELECT BasicTime.Period[from: t1, to: t2] FROM > 0 => result ฌ IF oldestFirst THEN $less ELSE $greater; < 0 => result ฌ IF oldestFirst THEN $greater ELSE $less; ENDCASE => result ฌ $equal; RETURN [result]; }; CompareStatus: PROC [s1, s2, status: TabPostOffice.Status, statusFirst: BOOLEAN ฌ TRUE] RETURNS [Basics.Comparison] ~ { result: Basics.Comparison; SELECT TRUE FROM s1 = status AND s2 # status => result ฌ IF statusFirst THEN $less ELSE $greater; s1 # status AND s2 = status => result ฌ IF statusFirst THEN $greater ELSE $less; ENDCASE => result ฌ $equal; RETURN [result]; }; GetPriorityRange: PROC [session: SessionInfo] RETURNS [low, high: INT] ~ { LowHigh: RedBlackTree.EachNode ~ { me: MsgEntry ฌ NARROW[data]; low ฌ MIN[low, me.info.priority]; high ฌ MAX[high, me.info.priority]; }; low ฌ LAST[INT]; high ฌ 0; RedBlackTree.EnumerateIncreasing[session.msgs, LowHigh]; }; AssignMsgIds: PROC [session: SessionInfo] RETURNS [total: INT] ~ { id: CARD ฌ 0; NextId: RedBlackTree.EachNode ~ { mi: MsgEntry ฌ NARROW[data]; mi.info.msgId ฌ id; id ฌ id + 1; }; RedBlackTree.EnumerateIncreasing[session.msgs, NextId]; total ฌ id; }; lookupKey: MsgEntry ฌ NEW[MsgEntryRecord]; LookupById: PROC [session: SessionInfo, msgId: INT] RETURNS [entry: MsgEntry] ~ { data: RedBlackTree.UserData; lookupKey.info.msgId ฌ msgId; data ฌ RedBlackTree.Lookup[session.msgs, lookupKey]; entry ฌ NARROW[data]; }; GetMsgEntry: PROC [walnutID: ROPE] RETURNS [msgEntry: MsgEntry] ~ { ENABLE WalnutDefs.Error => {PrintDebug["WALNUT ERROR: who=%s, code=%s, explanation=%s", IO.atom[who], IO.atom[code], IO.rope[explanation]]; CONTINUE}; GetAndCheckMsgSize: PROC [walnutID: ROPE] RETURNS [nat: NAT] = { CheckForNat: PROC [len: INT] RETURNS[nat: NAT] = { nat ฌ len }; len: INT ฌ WalnutOps.GetMsgSize[wH, walnutID].textLen; lengthThreshold: INT ฌ UserProfile.Number[key: "WalnutPostOffice.LengthThreshold", default: 5000]; IF len < lengthThreshold THEN nat ฌ len ELSE nat ฌ 0; }; size: NAT; fields: LoganBerry.Entry; msgEntry ฌ NEW[MsgEntryRecord]; msgEntry.walnutID ฌ walnutID; size ฌ GetAndCheckMsgSize[walnutID]; IF size # 0 THEN { msgEntry.text ฌ RefText.TrustTextAsRope[WalnutOps.GetMsgText[wH, walnutID, NIL]]; } ELSE { PrintDebug["Msg <%s> too long so only grabbing headers", IO.rope[walnutID]]; msgEntry.text ฌ RefText.TrustTextAsRope[WalnutOps.GetMsgHeaders[wH, walnutID, NIL]]; msgEntry.text ฌ Rope.Concat[msgEntry.text, "\n*** Message too long! ***\n"]; }; msgEntry.text ฌ CrsToNls[msgEntry.text]; fields ฌ TapMsgQueue.EntryFromMsg[TapFilter.ParseMsgIntoFields[msgEntry.text]]; msgEntry.info.msgId ฌ -1; -- don't know ID yet since it depends on sort order msgEntry.info.from ฌ LoganBerryEntry.GetAttr[fields, $from]; msgEntry.info.to ฌ LoganBerryEntry.GetAttr[fields, $to]; msgEntry.info.cc ฌ LoganBerryEntry.GetAttr[fields, $cc]; msgEntry.info.subject ฌ LoganBerryEntry.GetAttr[fields, $subject]; msgEntry.info.date ฌ LoganBerryEntry.GetAttr[fields, $date]; msgEntry.info.priority ฌ MsgInterestLevel[walnutID]; msgEntry.info.filters ฌ noFilters; msgEntry.info.status ฌ IF WalnutOps.GetHasBeenRead[wH, walnutID] THEN $Read ELSE $Unread; msgEntry.info.bodyLength ฌ Rope.Length[msgEntry.text]; msgEntry.info.bodyLines ฌ msgEntry.info.bodyLength/30; -- a very rough estimate }; MsgInterestLevel: PUBLIC PROC [msg: ROPE] RETURNS [ilevel: INT] ~ { Max: PROC [values: LIST OF ROPE] RETURNS [max: INT] ~ { max ฌ -1; FOR rL: LIST OF ROPE ฌ values, rL.rest WHILE rL # NIL DO i: INT ฌ LoganBerryEntry.V2I[rL.first]; IF i > max THEN max ฌ i; ENDLOOP; }; annotationDBName: ROPE ฌ UserProfile.Token[key: "WallTapestry.AnnotationDB", default: NIL]; ilevel ฌ -1; IF annotationDBName # NIL THEN BEGIN annot: TapFilter.Annotation; annot ฌ TapFilter.GetAnnotations[annotDB: annotationDBName, msgID: msg]; ilevel ฌ Max[LoganBerryEntry.GetAllAttrs[entry: annot, type: $Level]]; ilevel ฌ MAX[ilevel, Max[LoganBerryEntry.GetAllAttrs[entry: annot, type: $level]]]; END; IF ilevel = -1 THEN ilevel ฌ 50; }; GetBody: PROC [whole: ROPE] RETURNS [body: ROPE] ~ { i, previ: INT ฌ -1; WHILE i < Rope.Length[whole] DO previ ฌ i; i ฌ Rope.SkipTo[s: whole, pos: previ+1, skip: "\l\n\r"]; IF i = previ+1 THEN EXIT; ENDLOOP; body ฌ Rope.Substr[whole, i+1]; RETURN[body]; }; CrsToNls: PROC [old: ROPE] RETURNS [new: ROPE] ~ { TransCrsToNls: Rope.TranslatorType ~ { IF old = '\r THEN RETURN['\l] ELSE RETURN[old]; }; new ฌ Rope.Translate[base: old, translator: TransCrsToNls]; }; FmtPropI: PROC [v: TabPostOffice.PropInfoArray] RETURNS [rope: ROPE] ~ { rope ฌ NIL; FOR i: INT IN [0..v.size) DO IF rope#NIL THEN rope ฌ Rope.Concat[rope, "/"]; rope ฌ Rope.Cat[rope, v[i].p, "-", v[i].r]; rope ฌ Rope.Cat[rope, "-", v[i].v]; ENDLOOP; }; FmtPropA: PROC [v: TabPostOffice.PropertyArray] RETURNS [rope: ROPE] ~ { rope ฌ NIL; FOR i: INT IN [0..v.size) DO IF rope#NIL THEN rope ฌ Rope.Concat[rope, "/"]; rope ฌ Rope.Concat[rope, v[i]]; ENDLOOP; }; FmtWhence: PROC [v: TabPostOffice.Whences] RETURNS [rope: ROPE] ~ { SELECT v FROM $BOM => rope ฌ "BOM"; $BOB => rope ฌ "BOB"; ENDCASE; }; PrintDebug: PROC [format: Rope.ROPE ฌ NIL, v1, v2, v3, v4, v5: IO.Value ฌ [null[]]] ~ { SimpleFeedback.Append[$WalnutPostOffice, $begin, $Debug, "WalnutPostOffice: "]; SELECT TRUE FROM v1 = [null[]] => SimpleFeedback.Append[$WalnutPostOffice, $end, $Debug, format]; v2 = [null[]] => SimpleFeedback.PutF[$WalnutPostOffice, $end, $Debug, format, v1]; ENDCASE => SimpleFeedback.PutFL[$WalnutPostOffice, $end, $Debug, format, LIST[v1, v2, v3, v4, v5]]; }; ExportService: PROC [] RETURNS [] ~ { server ฌ TabPostOffice.MakeServer1Server [ data: NIL, tpostartsession: StartSession, tpogetmsginfo: GetMsgInfo, tpogetmsgtext: GetMsgText, tpochecknewmail: CheckNewMail, tpogetfolders: GetFolders, tpomarkmsg: MarkMsg, tpoendsession: EndSession ]; server ฌ SunRPCBinding.Export[unboundServer: server, transport: $TCP]; }; DoIt: Commander.CommandProc ~ { ExportService[]; IO.PutRope[cmd.out, "Walnut PostOffice server is now running.\n"]; }; Commander.Register["WalnutPostOfficeServer", DoIt, "Start Walnut PostOffice server"]; END.  WalnutPostOfficeServerImpl.mesa Copyright ำ 1992 by Xerox Corporation. All rights reserved. Doug Terry, July 22, 1993 11:47 am PDT This is an RPC server for accessing messages stored in a Walnut database. The RPC protocol is based on the notion of sessions. During each session, the client has access to a fixed set of messages. This set is specified when the session is started by a list of properties that must be held by the messages in the session (this can be thought of as a query over the message database). Messages within a session are accessed by a unique ID, a number in the range from 0 to one less than the number of messages selected. The ordering of messages in a session, i.e. the assignment of messages to IDs, is also specified when the session is started. The RPC protocol assumes that only a single session exists at any given time; it does not support multiple clients or multiple sessions per client. Types and Global Variables MsgInfo: TYPE = RECORD [ msgId: MsgID, from: ROPE, to: ROPE, cc: ROPE, date: ROPE, subject: ROPE, priority: Priority, filters: FilterSet, status: Status, bodyLength: INT32, bodyLines: INT32 ]; The set of messages that can be accessed during a session are stored in a RedBlackTree sorted by the desired sort-order (e.g. priority). Each element in this tree is of type MsgEntry. RPC Operations [o: Server1, numselects: INT32, plist: PropInfoArray, numorders: INT32, olist: PropertyArray] RETURNS [res: SessionResult] The plist specifies which messages are included in the new session, and the olist specifies the order in which these messages should be presented to the user. This procedures reads the messages from the database and builds a sorted RedBlack tree to hold information about the messages. A common case is that plist is empty. In this case, a simple priority threshold is used. The code is optimized by checking for this condition before retrieving other information about the message. [o: Server1, beginId: MsgID, endId: MsgID] RETURNS [res: MsgInfoResult] [o: Server1, msgId: MsgID, beginByte: INT32, numBytes: INT32, whence: Whences] RETURNS [res: MsgTextResult] [o: Server1] RETURNS [res: NewMailResult] [o: Server1] RETURNS [res: FoldersResult] [o: Server1, msgId: MsgID, markID: ROPE, markMsg: ROPE] RETURNS [res: ErrorInfo] [o: Server1] RETURNS [res: ErrorInfo] Selecting and Filtering Messages Finds all of the clauses on the selectList of the form: "folder", "equals", msgset. The list of msgsets are returned. Some folder names are ignored, e.g. "All", "AllMail", "NewMail". Quick check to see if the message's priority is above the threshold. Finds all of the clauses on the selectList of the form: "priority", relation, number. Returns true if this expression holds for the given msgEntry. This should eventually support other selection criteria as well. Sorting Messages When the RedBlackTree is being built up, it is sorted by the order specified in StartSession. After all entries have been added, session-specific IDs are assigned to the messages so that the tree is also sorted by message IDs. Thus, future lookups can simply compare message IDs. [data: UserData] RETURNS [Key] [k: Key, data: UserData] RETURNS [Basics.Comparison] The properties of the two messages are compared as specified on the orderList until a property is found for which the messages are not equal. If the messages are equal in all properties on the list then Walnut message IDs (which are known to be unique) are compared. Note: high priorities come before low priorities so the priorities are reversed when passed to Basics.CompareInt. [data: UserData] RETURNS [stop: BOOL ฌ FALSE] [data: UserData] RETURNS [stop: BOOL ฌ FALSE] Keep around a key so that a new one does not have to be allocated on each lookup; the info.msgId field simply needs to be assigned. Retrieving Information about Messages Retrieve from Walnut all of the information about a given message. WalnutOps.GetMsgText sometimes raises WalnutDefs.Error[$MsgTooLong] for messages that will not fit in a REF TEXT. We can't just catch this error, since then Walnut is left in a state where it thinks that a shutdown is necessary. So we must try to avoid this situation by first checking the length of the message. Returns the size of the message or 0 if the message will not fit in a NAT BEGIN ENABLE RuntimeError.BoundsFault => {nat ฌ 0; CONTINUE}; nat ฌ CheckForNat[len]; -- doesn't seem to work for some reason. END; Lookup the msg's ilevel in the annotation database. The default ilevel is used if a database entry is not found for this message. If the message is annotated with several ilevels then the highest one is used. Find two CRs in a row since that's where the body starts. Convert all CRs to Newlines since that's what UNIX wants. PROC [old: CHAR] RETURNS [CHAR] Debugging Output Exporting the Service ส>•NewlineDelimiter ™™Jšœ ฯeœ1™šžœžœžœž˜"K˜-K˜6Kšžœ˜—J˜K˜K˜K˜—š  œ%˜/Kš œžœ žœžœ žœžœžœ™kK˜K˜ŸK˜&K˜šžœžœ˜J˜—J˜K˜K˜K˜—š  œ'˜3Kšœ žœ™)K˜!Kšœžœ˜#K˜K˜K˜—Kšœžœžœ˜K˜š  œ%˜/Kšœ žœ™)Kš žœRžœ žœ žœžœ˜–Kšœžœžœžœ˜Kšœžœ˜K˜šžœ˜KšžœžœX˜hKšžœ%˜)—Kš žœžœžœžœžœ žœ˜JK˜Kšœžœ$˜5K˜š žœžœžœžœžœ˜5K˜Kšœ ž˜ Kšžœ˜—K˜K˜K˜K˜—š œ"˜)Kš œžœžœžœ žœžœ™PK˜oK˜K˜K˜K˜—š  œ%˜/Kšœ žœ™%K˜K˜K˜K˜K˜——™ Kšœžœ ˜K˜š  œžœ+žœ žœžœžœ˜cK™ธKšœ žœ˜šžœžœžœž˜%šžœ'žœžœ˜6Kšœžœ˜ Kš žœžœ'žœžœžœก˜Pšžœžœž˜Kšœ#žœ˜?Kšฯi'ะikข˜CKšข'ฃข˜CKšข#ฃข˜?Kšžœ˜$—Kšœ žœ˜ K˜—Kšžœ˜—šžœ žœžœ˜KšœWžœ˜lK˜—K˜K˜—š  œžœ žœ žœžœ žœ˜UK™DJšœ žœ˜Jšžœžœžœžœ˜#J˜&K˜ Kšžœ ˜K˜K˜—š  œžœ?žœ žœ˜pK™ึKšœ žœ˜Kš žœ žœžœžœžœ˜%šžœžœžœž˜%šžœ)žœžœ˜8Kšœ žœ˜KšœCžœ˜MKšžœžœžœก˜,šžœžœž˜K˜bK˜iK˜]K˜fK˜_Kšžœžœ˜—K˜—Kšžœ žœžœžœ˜Kšžœ˜—Kšžœ ˜K˜K˜——™K™™K˜š œก.˜NKšœžœ™Kšžœ˜ K˜—K˜š  œ˜%Kšœžœ™4K˜Kšœžœ˜Kšœžœ˜šžœžœ˜1Kšžœ=˜AKšžœ5˜9—Kšžœ ˜K˜K˜—–S0 ButtonLSFileName;/tilde/terry/Cedar/Hacking/TabPostOffice/TabPostOffice.mesa/š  œžœ<žœ˜lJ™‹K˜#šžœžœžœž˜$šžœžœž˜šœ%žœ˜/K˜=—šœ!žœ˜+K˜1—šœ!žœ˜+Kšœ2žœ˜9—šœ$žœ˜.Kšœ8žœ˜?—šœžœ˜)Kšœ.žœ˜5—šœ žœ˜*Kšœ=žœ˜C—šœ žœ˜*Kšœ=žœ˜D—šœ!žœ˜+Kšœ>žœ˜D—šœ#žœ˜-Kšœ>žœ˜E—Kšžœ˜—Kšžœžœžœ˜Kšžœ˜—šžœžœ˜K˜0—Jšžœ ˜K˜K˜—š œžœ žœžœ˜CJšœกYœก™qJšžœ˜#K˜K˜—š   œžœ žœžœžœžœ˜]K˜K˜Kšœ0žœ˜:Kšœ0žœ˜:šžœ$ž˜.Kšœžœ žœžœ ˜8Kšœžœ žœ žœ˜8Kšžœ˜—Jšžœ ˜K˜K˜—š   œžœ5žœžœžœ˜wK˜šžœžœž˜Kš œ žœžœ žœžœ ˜PKš œ žœžœ žœ žœ˜PKšžœ˜—Jšžœ ˜K˜K˜—š œžœžœ žœ˜Jš œ˜"Kšœžœžœžœ™-Kšœžœ˜Kšœžœ˜!Kšœžœ˜#K˜—Kšœžœžœ˜K˜ K˜8K˜K˜—š  œžœžœ žœ˜BKšœžœ˜ š œ˜!Kšœžœžœžœ™-Kšœžœ˜K˜K˜ K˜—K˜7K˜ K˜K˜—K™ƒKšœžœ˜*K˜š  œžœžœžœ˜QK˜K˜K˜4Kšœžœ˜K˜——™%š  œžœ žœžœ˜CK™BK™บKš žœRžœ žœ žœžœ˜–š  œžœ žœžœžœ˜@K™IKš   œžœžœžœžœ˜?Kšœžœ.˜6KšœžœN˜bšžœ˜Kšžœ ˜Kšžœ ˜ —šžœžœ'žœ™=K™AKšžœ™—K˜—Kšœžœ˜ K˜Kšœ žœ˜K˜K˜$šžœ ˜ šžœ˜KšœKžœ˜Q—šœžœ˜Jšœ9žœ˜LKšœNžœ˜TK˜M—K˜—K˜(K˜OKšœก3˜NK˜