<> <> <> <> DIRECTORY BasicTime USING [GMT, nullGMT, Unpacked, Now, Unpack], DefaultRegistry USING [UsersRegistry], GVBasics USING [ItemType, RName], GVMailParse, GVNames, GVSend USING [Abort, AddRecipient, AddToItem, CheckValidity, Create, Handle, Send, SendFailed, StartItem, StartSend, StartSendInfo], Icons, List USING [Append], Menus, IO, Rope, RuntimeError USING [BoundsFault], WalnutDocumentRope USING[ Create ], TiogaOps, ViewerOps, ViewerTools, UserCredentials USING [Get], WalnutSendOps, WalnutSendInternal, WalnutParse USING [MessageFieldIndex, MessageInfo]; WalnutSendMailImpl: CEDAR MONITOR IMPORTS BasicTime, DefaultRegistry, GVMailParse, GVNames, GVSend, List, IO, Rope, RuntimeError, WalnutDocumentRope, TiogaOps, ViewerOps, ViewerTools, UserCredentials, WalnutSendInternal, WalnutSendOps EXPORTS WalnutSendInternal, WalnutSendOps, WalnutParse = BEGIN OPEN WalnutSendInternal; <> defaultRegistry: PUBLIC ROPE_ DefaultRegistry.UsersRegistry[]; messageParseArray: PUBLIC ARRAY WalnutParse.MessageFieldIndex OF WalnutParse.MessageInfo _ [ ["Reply-to", simpleRope], -- this is really wrong, a special case for now ["Sender", simpleRope], ["From", simpleRope], ["To", rNameList], ["cc", rNameList], ["c", rNameList], ["bcc", rNameList], ["Date", simpleRope], ["Subject", simpleRope], ["Categories", rCatList], ["In-reply-to", simpleRope], ["VoiceFileID", simpleRope] ]; <<************************************************************************>> Send: PUBLIC PROC[v: Viewer, doClose: BOOL_ FALSE] RETURNS[sent: BOOL] = { oldMenu: Menus.Menu = v.menu; v.inhibitDestroy_ TRUE; BEGIN ENABLE UNWIND => GOTO out; ViewerOps.SetMenu[v, sendingMenu]; IF needToAuthenticate THEN { SenderReport["Authenticating user ..."]; IF ~AuthenticateUser[] THEN {ViewerOps.BlinkIcon[v, IF v.iconic THEN 0 ELSE 1]; ViewerOps.SetMenu[v, oldMenu]; v.inhibitDestroy_ FALSE; RETURN[FALSE] }; SenderReport[" ...ok\n"]; }; sent_ InternalSendMsg[v, doClose]; EXITS out => NULL; END; ViewerOps.SetMenu[v, oldMenu]; v.inhibitDestroy_ FALSE; }; <<* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *>> AppendHeaderLine: PUBLIC PROC[v: Viewer, line: ROPE, changeSelection: BOOL _ FALSE] = BEGIN ENABLE RuntimeError.BoundsFault => GOTO exit; text: ROPE; i: INT_ 0; ch: CHAR; TRUSTED {text_ WalnutDocumentRope.Create[LOOPHOLE [TiogaOps.ViewerDoc[v]]]}; DO -- find the double CR at the end of the headers UNTIL (ch_ text.Fetch[i]) = '\n DO i_ i + 1; ENDLOOP; IF (ch_ text.Fetch[i_ i + 1]) = '\n THEN EXIT; ENDLOOP; InsertIntoViewer[v, line, i, WalnutSendOps.labelFont, changeSelection]; ViewerTools.EnableUserEdits[v]; EXITS exit => SenderReport[IO.PutFR["Malformed headers; append of &g not done", IO.rope[line]]]; END; <<* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *>> AuthenticateUser: PROC RETURNS [BOOL] = BEGIN OPEN WalnutSendOps; auth: GVNames.AuthenticateInfo; IF userRName.Length[] = 0 THEN {SenderReport["Please Login\n"]; RETURN[FALSE]}; SELECT auth_ GVNames.Authenticate[userRName, UserCredentials.Get[].password] FROM group => SenderReport["... Can't login as group\n"]; individual => {needToAuthenticate_ FALSE; RETURN[TRUE]}; notFound => {SenderReport[userRName]; SenderReport[" is invalid - please Login\n"]}; allDown => SenderReport["... No server responded\n"]; badPwd => SenderReport["... Your Password is invalid - please Login\n"]; ENDCASE; RETURN[FALSE]; END; InternalSendMsg: PROC[senderV: Viewer, doClose: BOOL] RETURNS[sendOk: BOOL] = BEGIN status: SendParseStatus; sPos, mPos: INT; formatting: ROPE; smr: SendingRec; addedTxtLength: INT; newTxt: ROPE; contents, currentText: ViewerTools.TiogaContents; senderInfo: SenderInfo_ NARROW[ViewerOps.FetchProp[senderV, $SenderInfo]]; sendOk_ FALSE; senderInfo.aborted_ FALSE; IF senderInfo.successfullySent AND ~senderV.newVersion THEN { SenderReport["\nDo you really want to send this message again?"]; IF ~Confirmation[senderInfo] THEN { SenderReport[" .. Not sent\n"]; senderInfo.successfullySent_ FALSE; RETURN} }; senderInfo.successfullySent_ FALSE; SenderReport["... Parsing..."]; smr_ NEW[SendMsgRecObject]; TRUSTED {smr.fullText_ WalnutDocumentRope.Create[LOOPHOLE [TiogaOps.ViewerDoc[senderV]]]}; IF smr.fullText.Length[] > LAST[CARDINAL] THEN { SenderReport["\nMessage is too long to send; NOT sent\n"]; RETURN}; [status, sPos, mPos]_ ParseTextToBeSent[smr]; IF (status # ok) AND (status # includesPublicDL) THEN BEGIN SELECT status FROM fieldNotAllowed => IF sPos # mPos THEN { ShowErrorFeedback[senderV, sPos, mPos]; SenderReport[Rope.Substr[smr.fullText, MAX[0, sPos-1], mPos-sPos]]; SenderReport[" field is not allowed\n"]} ELSE SenderReport[IO.PutFR[" field at pos %g is not allowed\n", IO.int[sPos]]]; syntaxError => IF sPos # mPos THEN { ShowErrorFeedback[senderV, sPos, mPos]; SenderReport["\nSyntax error on line beginning with "]; SenderReport[Rope.Substr[smr.fullText, MAX[0, sPos-1], mPos-sPos]]} ELSE SenderReport[IO.PutFR["..... Syntax error at position %g ", IO.int[sPos]]]; includesPrivateDL => SenderReport[" Private dl's are not yet implemented\n"]; ENDCASE => ERROR; ViewerOps.BlinkIcon[senderV, IF senderV.iconic THEN 0 ELSE 1]; RETURN; END; IF CheckForAbortSend[senderInfo] THEN RETURN; IF (status = includesPublicDL OR smr.numRecipients > maxWithNoReplyTo) AND ~smr.replyTo THEN { howToReply: HowToReplyTo_ self; IF ~replyToSelf THEN { oldM: Menus.Menu_ senderV.menu; IF CheckForAbortSend[senderInfo] THEN RETURN; ViewerOps.SetMenu[senderV, replyToMenu]; SenderReport[ IO.PutFR["... %g public DLs and %g other recipients; please choose Reply-To option", IO.int[smr.numDLs], IO.int[smr.numRecipients]]]; SenderReport[ "\nClick Self to reply-to self, All to reply-to all, Cancel to cancel Send\n"]; howToReply_ ReplyToResponse[senderInfo]; ViewerOps.SetMenu[senderV, oldM]; }; IF howToReply # all THEN InsertIntoViewer[senderV, Rope.Concat["Reply-to: ", WalnutSendOps.userRName], smr.endHeadersPos, WalnutSendOps.labelFont]; IF howToReply = cancel THEN {SenderReport["\nDelivery cancelled. Reply-to: has been added\n"]; RETURN }; IF CheckForAbortSend[senderInfo] THEN RETURN; }; IF doClose AND ~senderV.iconic THEN ViewerOps.CloseViewer[senderV]; <> currentText_ ViewerTools.GetTiogaContents[senderV]; newTxt _ Rope.Cat[IF smr.from = NIL THEN "From: " ELSE "Sender: ", WalnutSendOps.userRName]; addedTxtLength _ newTxt.Length[]; InsertIntoViewer[senderV, newTxt, 0, WalnutSendOps.labelFont]; <> newTxt _ Rope.Concat["Date: ", RFC822Date[]]; addedTxtLength _ newTxt.Length[] + addedTxtLength + 1; InsertIntoViewer[senderV, newTxt, 0, WalnutSendOps.labelFont]; contents_ ViewerTools.GetTiogaContents[senderV]; <> { last: INT_ contents.contents.Length[] - 1; IF contents.contents.Fetch[last] = '\000 THEN -- NULL for padding { smr.fullText_ Rope.Substr[contents.contents, 1, last-1]; formatting_ Rope.Concat["\000", contents.formatting] } ELSE { smr.fullText_ Rope.Substr[contents.contents, 1]; formatting _ contents.formatting } }; IF smr.subject.Length[] > 40 THEN smr.subject_ Rope.Concat[Rope.Substr[smr.subject, 0, 35], " ..."]; smr.subject_ Rope.Cat[" \"", smr.subject, "\" "]; SenderReport["... Sending "]; SenderReport[smr.subject]; IF senderInfo.successfullySent_ sendOk_ SendIt[smr, senderInfo, formatting] THEN { SenderReport[smr.subject]; SenderReport[" has been delivered\n"]; senderInfo.prevMsg_ currentText; } ELSE { DeleteChars[senderV, addedTxtLength]; SenderReport[smr.subject]; SenderReport[" NOT sent\n"]; }; END; RFC822Date: PUBLIC PROC[gmt: BasicTime.GMT_ BasicTime.nullGMT] RETURNS[date: ROPE] = <> BEGIN OPEN IO; upt: BasicTime.Unpacked_ BasicTime.Unpack[IF gmt = BasicTime.nullGMT THEN BasicTime.Now[] ELSE gmt]; zone: ROPE; month, tyme, year: ROPE; timeFormat: ROPE = "%02g:%02g:%02g %g"; -- "hh:mm:ss zzz" dateFormat: ROPE = "%2g %g %g %g"; -- "dd mmm yy timeFormat" arpaNeg: BOOL_ upt.zone > 0; aZone: INT_ ABS[upt.zone]; zDif: INT_ aZone / 60; zMul: INT_ zDif * 60; IF (zMul = aZone) AND arpaNeg THEN { IF upt.dst = yes THEN SELECT zDif FROM 0 => zone_ "UT"; 4 => zone_ "EDT"; 5 => zone_ "CDT"; 6 => zone_ "MDT"; 8 => zone_ "PDT"; ENDCASE ELSE SELECT zDif FROM 0 => zone_ "UT"; 5 => zone_ "EST"; 6 => zone_ "CST"; 7 => zone_ "MST"; 8 => zone_ "PST"; ENDCASE; }; IF zone = NIL THEN { mm: INT_ aZone - zMul; zone_ PutFR[IF arpaNeg THEN "-%02g%02g" ELSE "+%02g%02g", int[zDif], int[mm]]; }; SELECT upt.month FROM January => month_ "Jan"; February => month_ "Feb"; March => month_ "Mar"; April => month_ "Apr"; May => month_ "May"; June => month_ "Jun"; July => month_ "Jul"; August => month_ "Aug"; September => month_ "Sep"; October => month_ "Oct"; November => month_ "Nov"; December => month_ "Dec"; unspecified => ERROR; ENDCASE => ERROR; year_ Rope.Substr[PutFR[NIL, int[upt.year]], 2]; tyme_ PutFR[timeFormat, int[upt.hour], int[upt.minute], int[upt.second], rope[zone]]; date_ PutFR[dateFormat, int[upt.day], rope[month], rope[year], rope[tyme]]; END; ShowErrorFeedback: PUBLIC PROC[v: Viewer, start, end: INT] = BEGIN OPEN TiogaOps; ENABLE UNWIND => GOTO exit; startLoc, endLoc: Location; thisV: Ref = ViewerDoc[v]; beginning: Location_ [FirstChild[thisV], 0]; startLoc_ LocRelative[beginning, start]; endLoc_ LocRelative[beginning, end]; SetSelection[viewer: v, start: startLoc, end: endLoc, which: feedback]; EXITS exit => NULL; END; InsertIntoViewer: PUBLIC PROC [v: Viewer, what: ROPE, where: INT, labelFont: ROPE _ NIL, changeSelection: BOOL _ FALSE] = BEGIN OPEN TiogaOps; thisV: Ref = ViewerDoc[v]; prefix: ROPE; InsertChars: PROC[root: Ref] = BEGIN insertLoc: Location; prevV: Viewer; prevStart, prevEnd: Location; prevLevel: SelectionGrain; cb, pd: BOOL; IF where < 0 THEN insertLoc_ LastLocWithin[LastChild[thisV]] ELSE insertLoc_ LocRelative[[FirstChild[thisV], 0], where]; [prevV, prevStart, prevEnd, prevLevel, cb, pd]_ GetSelection[primary]; ViewerTools.EnableUserEdits[v]; SelectPoint[v, insertLoc, primary]; IF labelFont # NIL THEN TiogaOps.SetLooks[labelFont, caret]; TiogaOps.InsertRope[prefix]; IF labelFont # NIL THEN TiogaOps.ClearLooks[caret]; TiogaOps.InsertRope[what]; TiogaOps.Break[]; -- make a new node ViewerTools.InhibitUserEdits[v]; IF ~changeSelection AND (prevV # v) AND (prevV#NIL) AND ~prevV.destroyed THEN SetSelection[prevV, prevStart, prevEnd, prevLevel, cb, pd ! TiogaOps.SelectionError => CONTINUE]; END; LockViewerOpen[v]; prefix _ Rope.Substr[ what, 0, Rope.Find[s1: what, s2: ":"] ]; what _ Rope.Replace[base: what, start: 0, len: prefix.Length, with: NIL]; IF thisV = NIL THEN InsertChars[thisV] ELSE CallWithLocks[InsertChars, thisV]; ReleaseOpenViewer[v]; END; DeleteChars: PUBLIC ENTRY PROC[v: Viewer, num: INT] = BEGIN ENABLE UNWIND => NULL; IF num # 0 THEN DeleteLeadingChars[v, num] END; DeleteLeadingChars: INTERNAL PROC[v: Viewer, num: INT] = BEGIN OPEN TiogaOps; thisV: Ref_ ViewerDoc[v]; DelChars: PROC[root: Ref] = BEGIN prevV: Viewer; prevStart, prevEnd: Location; prevLevel: SelectionGrain; cb, pd: BOOL; startLoc: Location_ [FirstChild[thisV], 0]; endLoc: Location_ LocRelative[startLoc, num]; [prevV, prevStart, prevEnd, prevLevel, cb, pd]_ GetSelection[primary]; ViewerTools.EnableUserEdits[v]; SetSelection[viewer: v, start: startLoc, end: endLoc]; Delete[]; GoToNextNode[]; Join[]; IF (prevV # v) AND (prevV#NIL) THEN SetSelection[prevV, prevStart, prevEnd, prevLevel, cb, pd]; END; IF thisV = NIL THEN DelChars[thisV] ELSE CallWithLocks[DelChars, thisV]; END; <<* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *>> SendIt: PROC[smr: SendingRec, senderInfo: SenderInfo, formatting: ROPE] RETURNS[ sent: BOOLEAN] = { InitSending: ENTRY PROC = { ENABLE UNWIND => NULL; senderInfo.sendHandle_ GVSend.Create[]; senderInfo.validateFlag_ TRUE; senderInfo.aborted_ FALSE; }; FinishedSending: ENTRY PROC = { ENABLE UNWIND => NULL; senderInfo.sendHandle_ NIL }; InitSending[]; sent _ SendMessage[smr, senderInfo, formatting ! GVSend.SendFailed => { IF notDelivered THEN { SenderReport["\nCommunication failure during send. Retry?\n"]; IF Confirmation[senderInfo] THEN RETRY ELSE { sent _ FALSE; CONTINUE } } ELSE { SenderReport["\nCommunication failure, but message delivered\n"]; sent _ TRUE; CONTINUE; }; }]; FinishedSending[]; } ; SendMessage: PROC[smr: SendingRec, senderInfo: SenderInfo, formatting: ROPE] RETURNS[ sent: BOOLEAN ] = { DO ENABLE GVSend.SendFailed => CONTINUE; -- try again if SendFailed h: GVSend.Handle _ senderInfo.sendHandle; startInfo: GVSend.StartSendInfo; stepper: LIST OF RName; tempInteger: INT; numRecips: INT _ 0; numValidRecips: INT; firstInvalidUser: BOOL_ TRUE; numInvalidUsers: INTEGER_ 0; InvalidUserProc: PROC [ userNum: INT, userName: RName ] = { IF firstInvalidUser THEN {SenderReport["\nInvalid user(s): "]; firstInvalidUser_ FALSE}; SELECT numInvalidUsers _ numInvalidUsers + 1 FROM 1 => SenderReport[userName]; IN [2..5] => {SenderReport[", "]; SenderReport[userName]}; 6 => SenderReport[", ...\n"]; ENDCASE; } ; sent _ FALSE ; startInfo _ GVSend.StartSend[ handle: senderInfo.sendHandle, senderPwd: UserCredentials.Get[].password, sender: WalnutSendOps.userRName, returnTo: WalnutSendOps.userRName, validate: senderInfo.validateFlag ] ; SELECT startInfo FROM badPwd => {SenderReport["\nInvalid password\n"]; RETURN}; badSender => {SenderReport["\nInvalid sender name\n"]; RETURN}; badReturnTo => {SenderReport["\nBad return-to field\n"]; RETURN}; allDown => {SenderReport["\nAll servers are down\n"]; RETURN}; ok => { stepper _ smr.to; WHILE stepper # NIL DO GVSend.AddRecipient[ h, stepper.first ]; numRecips _ numRecips + 1; stepper _ stepper.rest; ENDLOOP; IF CheckForAbortSend[senderInfo] THEN RETURN; stepper _ smr.cc; WHILE stepper # NIL DO GVSend.AddRecipient[ h, stepper.first ] ; numRecips _ numRecips + 1 ; stepper _ stepper.rest ; ENDLOOP ; IF CheckForAbortSend[senderInfo] THEN RETURN; IF senderInfo.validateFlag THEN { IF (numValidRecips_ GVSend.CheckValidity[ h, InvalidUserProc]) = 0 THEN { SenderReport["\nThere were NO valid recipients; do you wish to send anyway?\n"]; IF CheckForAbortSend[senderInfo] OR ~Confirmation[senderInfo] THEN {GVSend.Abort[h]; RETURN}; }; IF numValidRecips # numRecips THEN { tempInteger _ numRecips-numValidRecips; SenderReport[IO.PutFR["\nThere were %g invalid recipients,", IO.int[tempInteger] ]]; SenderReport[" do you wish to send anyway?\n"]; IF CheckForAbortSend[senderInfo] OR ~Confirmation[senderInfo] THEN {GVSend.Abort[h]; RETURN}; }; }; IF CheckForAbortSend[senderInfo] THEN RETURN; SenderReport[IO.PutFR["..sending to %g recipients\n", IO.int[numValidRecips]]]; senderInfo.validateFlag_ FALSE; -- if sending fails, don't need to re-validate GVSend.StartItem[h, Text]; GVSend.AddToItem[h, smr.fullText]; IF formatting#NIL THEN { -- send the formatting info as a second item GVSend.StartItem[h, WalnutSendOps.TiogaCTRL]; GVSend.AddToItem[h, formatting] }; IF smr.voiceID.Length[] # 0 THEN { GVSend.StartItem[h, Audio]; GVSend.AddToItem[h, smr.voiceID] }; ViewerOps.SetMenu[senderInfo.senderV, blankMenu]; IF CheckForAbortSend[senderInfo] THEN RETURN; GVSend.Send[ h ] ; sent_ TRUE; RETURN; } ; ENDCASE ; ENDLOOP; } ; <<* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *>> CanonicalName: PUBLIC PROC [simpleName, registry: ROPE] RETURNS[name: ROPE] = BEGIN name_ simpleName; IF registry.Length[] = 0 THEN name_ name.Cat[".", defaultRegistry] ELSE name_ name.Cat[".", registry]; END; ParseTextToBeSent: PROC[msg: SendingRec] RETURNS[status: SendParseStatus, sPos, mPos: INT] = BEGIN OPEN GVMailParse; mLF: WalnutParse.MessageInfo; tHeaders: LIST OF ROPE_ NIL; msgText: ROPE_ msg.fullText; lastCharPos: INT_ msgText.Length[] - 1; lastCharIsCR: BOOL_ (msgText.Fetch[lastCharPos] = '\n); countOfRecipients, dlCount: INT_ 0; GetNextMsgChar: PROC[] RETURNS [ch: CHAR] = { IF mPos <= lastCharPos THEN ch_ Rope.Fetch[msgText, mPos] ELSE IF (mPos=lastCharPos+1) AND ~lastCharIsCR THEN ch_ '\n ELSE ch_ endOfInput; mPos_ mPos + 1; }; RNameListField: PROC[index: WalnutParse.MessageFieldIndex] = BEGIN fieldBody, fbEnd: LIST OF RName_ NIL; AnotherRName: PROC[r1, r2: ROPE, isFile, isNested: BOOL] RETURNS [ROPE, BOOLEAN] = BEGIN name: ROPE; name_ CanonicalName[r1, r2]; IF fbEnd=NIL THEN fbEnd_ fieldBody_ CONS[name, NIL] ELSE fbEnd_ fbEnd.rest_ CONS[name, NIL]; IF isFile THEN status_ includesPrivateDL ELSE IF name.Find["^"] < 0 THEN countOfRecipients_ countOfRecipients + 1 ELSE { IF status # includesPrivateDL THEN status_ includesPublicDL; dlCount_ dlCount + 1 }; RETURN[NIL, FALSE]; END; ParseNameList[pH, GetNextMsgChar, AnotherRName, NIL]; SELECT index FROM toF => IF msg.to = NIL THEN msg.to_ fieldBody ELSE IF fieldBody#NIL THEN TRUSTED {msg.to_ LOOPHOLE[List.Append[LOOPHOLE[msg.to], LOOPHOLE[fieldBody]]]}; ccF, cF, bccF => IF msg.cc = NIL THEN msg.cc_ fieldBody ELSE IF fieldBody#NIL THEN TRUSTED {msg.cc_ LOOPHOLE[List.Append[LOOPHOLE[msg.cc], LOOPHOLE[fieldBody]]]}; fromF => msg.from_ fieldBody.first; -- needs to be non-NIL ENDCASE => ERROR; END; pH: ParseHandle; field: ROPE_ NIL; fieldNotRecognized: BOOL; mPos_ 0; -- where we are in the fulltext status_ ok; -- start with good status pH_ InitializeParse[]; DO sPos_ mPos; [field, fieldNotRecognized]_ GetFieldName[pH, GetNextMsgChar ! ParseError => { FinalizeParse[pH]; GOTO errorExit}]; IF ~fieldNotRecognized THEN EXIT; IF Rope.Equal[field, "Sender", FALSE] OR Rope.Equal[field, "Date", FALSE] THEN RETURN[fieldNotAllowed, sPos, mPos]; FOR i: WalnutParse.MessageFieldIndex IN WalnutParse.MessageFieldIndex DO { mLF_ messageParseArray[i]; IF Rope.Equal[messageParseArray[i].name, field, FALSE] THEN -- ignore case { fieldNotRecognized_ FALSE; SELECT mLF.fType FROM simpleRope => SELECT i FROM fromF => RNameListField[i ! ParseError => GOTO errorExit]; replyToF => {msg.replyTo_ TRUE; []_ GetFieldBody[pH, GetNextMsgChar, TRUE]}; subjectF => msg.subject_ GetFieldBody[pH, GetNextMsgChar]; voiceF => msg.voiceID_ GetFieldBody[pH, GetNextMsgChar]; ENDCASE => []_ GetFieldBody[pH, GetNextMsgChar, TRUE]; rCatList => []_ GetFieldBody[pH, GetNextMsgChar, TRUE]; rNameList => RNameListField[i ! ParseError => GOTO errorExit]; ENDCASE => ERROR; EXIT }; }; ENDLOOP; IF fieldNotRecognized THEN []_ GetFieldBody[pH, GetNextMsgChar]; -- skip anything not recognized ENDLOOP; <> FinalizeParse[pH]; msg.endHeadersPos_ mPos - 1; <> <> msg.numRecipients_ countOfRecipients; msg.numDLs_ dlCount; EXITS errorExit => RETURN[syntaxError, sPos, mPos]; END; END.