<<>> <> <> <> <<>> <> <> <> DIRECTORY Commander, InputFocus USING [GetInputFocus], IO, MailAnswer USING [MakeHeader], MailBasics, MailParse, Menus, PFS, Process USING [Detach], Rope, RuntimeError USING [BoundsFault], SendMailParseMsg, SendMailInternal, SendMailOps, TEditInputOps USING [DoFindPlaceholders], Tioga, TiogaOps USING [ Ref, Location, ViewerDoc, CallWithLocks, CancelSelection, CaretBefore, Delete, FindText, GetSelection, InsertRope, SelectDocument, SelectionRoot, SelectPoint, SetSelection, SetSelectionLooks, ToPrimary], ViewerOps, ViewerClasses, ViewerEvents USING [EventProc, RegisterEventProc, UnRegisterEventProc], ViewerTools; SendMailOpsImpl: CEDAR MONITOR IMPORTS Commander, IO, PFS, Process, Rope, RuntimeError, MailAnswer, MailParse, SendMailInternal, SendMailOps, TEditInputOps, TiogaOps, InputFocus, Menus, ViewerEvents, ViewerOps, ViewerTools EXPORTS SendMailInternal, SendMailOps, SendMailParseMsg SHARES ViewerClasses = BEGIN OPEN SendMailOps, SendMailInternal, SendMailParseMsg; <<************************************************************************>> ROPE: TYPE = Rope.ROPE; Viewer: TYPE = ViewerClasses.Viewer; STREAM: TYPE = IO.STREAM; TiogaContents: TYPE = ViewerTools.TiogaContents; viewerStart: ViewerTools.SelPos = NEW[ViewerTools.SelPosRec ¬ [0, 0]]; nullIndex: INT = LAST[INT]; messageParseArray: PUBLIC ARRAY MessageFieldIndex OF 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] ]; sendMailDebug: BOOL ¬ FALSE; Answer: PUBLIC PROC[msgHeaders: ROPE, who: Viewer ¬ NIL, which: ATOM] RETURNS [v: Viewer] = { notOk: BOOL; errorIndex: INT; answer: ROPE; answerForm: SendMailOps.Form; thisUser: MailBasics.RName ¬ [$none, NIL]; AnswerGetChar: PROC[pos: INT] RETURNS[CHAR] = { ch: CHAR ~ msgHeaders.Fetch[pos]; RETURN[IF ch = '\l THEN '\r ELSE ch]; }; IF ( which # NIL ) THEN { IF userRNameList = NIL THEN DoUserNameAndRegistry[]; IF userRNameList = NIL THEN SenderReport["\n*** You have not logged in; answer form will not be completely correct\n"]; FOR rL: MailBasics.RNameList ¬ userRNameList, rL.rest UNTIL rL = NIL DO IF ( which # rL.first.ns ) THEN LOOP; thisUser ¬ rL.first; EXIT; ENDLOOP; }; IF sendMailDebug THEN { SenderReport["\n*** about to answer:\n%g\n*****\n", [rope[msgHeaders]] ]; }; [notOk, answer, errorIndex] ¬ MailAnswer.MakeHeader[which, AnswerGetChar, msgHeaders.Length[], thisUser]; IF sendMailDebug THEN { SenderReport["\n*** got the answer:\n%g\n*****\n", [rope[answer]] ]; }; IF notOk THEN { start, end: INT ¬ errorIndex; BEGIN ENABLE RuntimeError.BoundsFault => {start ¬ 0; end ¬ 1; CONTINUE}; IF start = nullIndex THEN start ¬ 0 ELSE {UNTIL msgHeaders.Fetch[start] = '\r DO start ¬ start - 1; ENDLOOP; start ¬ start + 1}; IF end = nullIndex THEN end ¬ start + 1 ELSE {UNTIL msgHeaders.Fetch[end] = '\r DO end ¬ end + 1; ENDLOOP; end ¬ end - 1}; END; IF who # NIL THEN ShowErrorFeedback[who, start, end]; SenderReport[ "\nSyntax error in header line \"%g\"", [rope[msgHeaders.Substr[start, end-start+1] ]] ]; IF answer.Length[] = 0 THEN RETURN; SenderReport["\n*****Partial answer has been generated\n"]; }; answerForm ¬ NEW[ SendMailOps.FormRec ¬ [formText: answerText, fields: ParseAnswerHeader[answer] ] ]; v ¬ BuildSendViewer[TRUE, FALSE, answerForm, who, alwaysNewSender].v; ClearFileAssoc[v]; GrabFocus[v]; }; ParseAnswerHeader: PROC[header: ROPE] RETURNS[fields: LIST OF ROPE] = { HeaderLines: ARRAY[0..3] OF Rope.ROPE; startPos: INT ¬ Rope.Find[header, ": ", 0]; endOfLine: INT ¬ Rope.Find[header, "\r", startPos]; FOR i: INT IN [0..3] DO HeaderLines[i] ¬ Rope.Substr[header, startPos+2, endOfLine-startPos-2]; startPos ¬ Rope.Find[header, ": ", endOfLine]; IF startPos = -1 THEN EXIT ELSE endOfLine ¬ Rope.Find[header, "\r", startPos] ENDLOOP; RETURN[ LIST[ HeaderLines[0], HeaderLines[1], HeaderLines[2], HeaderLines[3] ] ] }; smallBoldLooks: Tioga.Looks ¬ ALL[FALSE]; Forward: PUBLIC PROC[msg: Viewer, who: Viewer _ NIL] RETURNS[v: Viewer] = { forwardForm: SendMailOps.Form = NEW[SendMailOps.FormRec _ [formText: forwardText, fields: NIL]]; pstart, pend: TiogaOps.Location; root: TiogaOps.Ref; found: BOOL; v _ BuildSendViewer[TRUE, FALSE, forwardForm, who, alwaysNewSender].v; ClearFileAssoc[v]; TiogaOps.SelectDocument[v]; root _ TiogaOps.SelectionRoot[]; pstart _ TiogaOps.Location[ root, 0 ]; TiogaOps.SelectPoint[ v, pstart ]; found _ TiogaOps.FindText[v, "MessageHeader"]; IF found THEN { [ , pstart, pend, , , ] _ TiogaOps.GetSelection[]; TiogaOps.SetSelection[viewer: v, start: pstart, end: pend, level: word, pendingDelete: TRUE]; TiogaOps.InsertRope[ msg.name ] }; found _ TiogaOps.FindText[v, "ForwardedMessage"]; IF NOT found THEN RETURN; [ , pstart, pend, , , ] _ TiogaOps.GetSelection[]; TiogaOps.SetSelection[viewer: v, start: pstart, end: pend, level: node, pendingDelete: TRUE ]; TiogaOps.SelectDocument[ viewer: msg, which: secondary ]; TiogaOps.ToPrimary[]; UnsetNewVersion[v]; GrabFocus[v] }; original: ROPE ~ "Original-"; originally: ROPE ~ "Originally-"; ReSend: PUBLIC PROC[msg: Viewer, who: Viewer ¬ NIL] RETURNS[v: ViewerClasses.Viewer] = { reSendForm: SendMailOps.Form = NEW[SendMailOps.FormRec ¬ [formText: SendMailOps.GetCRTiogaContents[msg], fields: NIL]]; AddPrefix: PROC[what, prefix: ROPE] ~ { IF TiogaOps.FindText[viewer: v, rope: what, whichDir: anywhere, case: FALSE] THEN { TiogaOps.CaretBefore[]; TiogaOps.SetSelectionLooks[]; TiogaOps.InsertRope[prefix]; }; }; v ¬ BuildSendViewer[TRUE, FALSE, reSendForm, who, alwaysNewSender].v; ClearFileAssoc[v]; TiogaOps.SelectDocument[v]; AddPrefix["Date:", original]; AddPrefix["Sender:", original]; AddPrefix["From:", originally]; AddPrefix["Message-ID:", original]; UnsetNewVersion[v]; }; ClearFileAssoc: PUBLIC PROC[v: Viewer] = { IF v.file # NIL THEN v.file ¬ NIL; v.name ¬ sendCaption; ViewerOps.PaintViewer[v, caption]; }; SenderNewVersion: PUBLIC PROC[viewer: Viewer] = { OPEN Menus; menu: Menus.Menu = viewer.menu; <> firstForm: Menus.MenuEntry ¬ NIL; getEntry: Menus.MenuEntry ¬ Menus.FindEntry[menu, "Get"]; IF getEntry # NIL THEN Menus.SetGuarded[getEntry, TRUE]; getEntry ¬ Menus.FindEntry[menu, "Default"]; IF getEntry # NIL THEN Menus.SetGuarded[getEntry, TRUE]; getEntry ¬ Menus.FindEntry[menu, "PrevMsg"]; IF getEntry # NIL THEN Menus.SetGuarded[getEntry, TRUE]; FOR i: Menus.MenuLine IN [1..5) DO thisLine: Menus.MenuEntry = Menus.GetLine[menu, i]; IF thisLine = NIL THEN EXIT; IF firstForm = NIL THEN EXIT; IF Rope.Equal[thisLine.name, firstForm.name] THEN FOR entry: MenuEntry ¬ thisLine, entry.link UNTIL entry = NIL DO SetGuarded[entry, TRUE] ENDLOOP; ENDLOOP; ViewerOps.PaintViewer[viewer, menu]; -- show as guarded viewer.newVersion ¬ TRUE; }; UnsetNewVersion: PUBLIC PROC[viewer: Viewer] = { OPEN Menus; menu: Menu = viewer.menu; firstForm: Menus.MenuEntry ¬ NIL; <> getEntry: Menus.MenuEntry ¬ Menus.FindEntry[menu, "Get"]; IF getEntry # NIL THEN Menus.SetGuarded[getEntry, FALSE]; getEntry ¬ Menus.FindEntry[menu, "Default"]; IF getEntry # NIL THEN Menus.SetGuarded[getEntry, FALSE]; getEntry ¬ Menus.FindEntry[menu, "PrevMsg"]; IF getEntry # NIL THEN Menus.SetGuarded[getEntry, FALSE]; FOR i: Menus.MenuLine IN [1..5) DO thisLine: Menus.MenuEntry = Menus.GetLine[menu, i]; IF thisLine = NIL THEN EXIT; IF firstForm = NIL THEN EXIT; IF Rope.Equal[thisLine.name, firstForm.name] THEN FOR entry: MenuEntry ¬ thisLine, entry.link UNTIL entry = NIL DO SetGuarded[entry, FALSE] ENDLOOP; ENDLOOP; ViewerOps.PaintViewer[viewer, menu]; -- show as unguarded viewer.newVersion ¬ FALSE; ViewerOps.PaintViewer[viewer, caption]; }; <<* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *>> TiogaTextFromStrm: PUBLIC PROC [strm: IO.STREAM, startPos: INT ¬ 0, len: INT ¬ LAST[INT]] RETURNS [contents: TiogaContents] = { <> ENABLE IO.EndOfStream => GOTO TooShort; fulltext: ROPE ¬ RopeFromStream[strm, startPos, len]; formatPos: INT ¬ Rope.Index[fulltext, 0, "\000\000"]; contents ¬ NEW[ViewerTools.TiogaContentsRec]; contents.contents ¬ Rope.Substr[fulltext, 0, formatPos]; IF formatPos < Rope.Length[fulltext] THEN contents.formatting ¬ Rope.Substr[fulltext, formatPos]; EXITS TooShort => RETURN[NIL]; }; RopeFromStream: PUBLIC PROC [strm: STREAM, startPos, len: INT] RETURNS [contents: ROPE] = { <> rem: INT ¬ IO.GetLength[strm] - startPos; IF rem < len THEN len ¬ rem; IF len < 2048 THEN TRUSTED { <> nat: NAT ¬ len; text: Rope.Text ¬ Rope.NewText[nat]; IO.SetIndex[strm, startPos]; [] ¬ IO.GetBlock[strm, LOOPHOLE[text], 0, nat]; RETURN [text]; }; <> rem ¬ len/2; len ¬ len - rem; contents ¬ RopeFromStream[strm, startPos, len]; <> contents ¬ Rope.Concat[contents, RopeFromStream[strm, startPos+len, rem] ]; }; GetFieldBody: PUBLIC PROC[text, fieldName: ROPE] RETURNS[fieldBody: ROPE] = { <> OPEN MailParse; mPos: INT ¬ 0; lastCharPos: INT ¬ text.Length[]; pH: MailParse.ParseHandle ¬ MailParse.InitializeParse[]; NextChar: PROC[] RETURNS [ch: CHAR] = { IF mPos > lastCharPos THEN ch ¬ endOfInput ELSE ch ¬ text.Fetch[mPos]; mPos ¬ mPos + 1; }; BEGIN field: ROPE ¬ NIL; DO found, fieldNotRecognized: BOOL ¬ TRUE; field ¬ MailParse.GetFieldName[pH, NextChar ! ParseError => GOTO parseErrorExit]; IF field = NIL THEN EXIT; IF Rope.Equal[fieldName, field, FALSE] THEN -- ignore case { fieldBody ¬ MailParse.GetFieldBody[pH, NextChar]; EXIT} ELSE [] ¬ MailParse.GetFieldBody[pH, NextChar, TRUE]; ENDLOOP; MailParse.FinalizeParse[pH]; EXITS parseErrorExit => { MailParse.FinalizeParse[pH]; RETURN[NIL]}; END; }; GetRecipients: PUBLIC PROC[sender: Viewer, transport: ATOM ¬ NIL] RETURNS[rList: MailBasics.RNameList, parseError: BOOL] = { <> text: ROPE; status: SendParseStatus; sPos, mPos: INT; TRUSTED {text ¬ SendMailOps.CreateRopeForTextNode[LOOPHOLE [TiogaOps.ViewerDoc[sender]]]}; [status, sPos, mPos, rList] ¬ Parse[text, transport]; IF (status # ok) AND (status # includesPublicDL) THEN BEGIN SELECT status FROM fieldNotAllowed => IF sPos # mPos THEN { ShowErrorFeedback[sender, sPos, mPos]; SenderReport[" %g field is not allowed\n", [rope[Rope.Substr[text, MAX[0, sPos-1], mPos-sPos]]] ] } ELSE SenderReport[" field at pos %g is not allowed\n", [integer[sPos]] ]; syntaxError => IF sPos # mPos THEN { ShowErrorFeedback[sender, sPos, mPos]; SenderReport["\nSyntax error on line beginning with %g", [rope[Rope.Substr[text, MAX[0, sPos-1], mPos-sPos]] ] ]; } ELSE SenderReport["..... Syntax error at position %g ", [integer[sPos]] ]; includesPrivateDL => SenderReport[" Private dl's are not yet implemented\n"]; ENDCASE => ERROR; ViewerOps.BlinkIcon[sender, IF sender.iconic THEN 0 ELSE 1]; RETURN[NIL, TRUE] END; RETURN[rList, FALSE] }; Parse: PUBLIC PROC[text: ROPE, transport: ATOM] RETURNS[status: SendParseStatus, sPos, mPos: INT, rList: MailBasics.RNameList] = { smr: SendingRec ¬ NEW[SendMsgRecObject]; smr.fullText ¬ text; [status, sPos, mPos] ¬ ParseText[smr, transport]; IF ( status = fieldNotAllowed ) OR ( status = syntaxError ) THEN RETURN; IF smr.cc = NIL THEN { rList ¬ smr.to; RETURN}; IF smr.to = NIL THEN { rList ¬ smr.cc; RETURN}; <> BEGIN rL: MailBasics.RNameList ¬ smr.cc; -- cc usually shorter?? rList ¬ smr.cc; UNTIL rL.rest = NIL DO rL ¬ rL.rest; ENDLOOP; rL.rest ¬ smr.to; END; }; ParseHeadersFromRope: PUBLIC PROC[headers: ROPE, proc: ParseProc] RETURNS[msgHeaders: MsgHeaders] = { <> OPEN MailParse; mPos: INT ¬ 0; len: INT ¬ headers.Length[]; pH: MailParse.ParseHandle ¬ MailParse.InitializeParse[]; NextChar: PROC[] RETURNS [ch: CHAR] = { IF mPos >= len THEN ch ¬ endOfInput ELSE ch ¬ headers.Fetch[mPos]; mPos ¬ mPos + 1; }; msgHeaders ¬ NIL; IF headers.Fetch[0] = '\r THEN mPos ¬ 1; -- ignore initial CR (tioga formatting nonsense) { ENABLE ParseError => GOTO parseErrorExit; fieldName: ROPE ¬ NIL; wantThisField, continue: BOOL ¬ TRUE; DO fieldName ¬ MailParse.GetFieldName[pH, NextChar]; IF fieldName = NIL THEN EXIT; IF proc # NIL THEN [wantThisField, continue] ¬ proc[fieldName]; IF wantThisField THEN msgHeaders ¬ CONS[[fieldName, MailParse.GetFieldBody[pH, NextChar]], msgHeaders] ELSE [] ¬ MailParse.GetFieldBody[pH, NextChar, TRUE]; IF ~continue THEN EXIT; ENDLOOP; MailParse.FinalizeParse[pH]; EXITS parseErrorExit => { MailParse.FinalizeParse[pH]; RETURN[msgHeaders]}; }; }; ParseMsgFromStream: PUBLIC PROC[strm: IO.STREAM, len: INT, proc: ParseProc] RETURNS[msgHeaders: MsgHeaders] = { <> OPEN MailParse; mPos: INT ¬ 0; pH: MailParse.ParseHandle ¬ MailParse.InitializeParse[]; NextChar: PROC[] RETURNS [ch: CHAR] = { IF mPos > len THEN ch ¬ endOfInput ELSE ch ¬ strm.GetChar[ ! IO.EndOfStream => { mPos ¬ len; ch ¬ endOfInput; CONTINUE } ]; mPos ¬ mPos + 1; }; msgHeaders ¬ NIL; IF strm.PeekChar[] = '\r THEN { -- ignore initial CR (tioga formatting nonsense) [] ¬ strm.GetChar[]; mPos ¬ 1; }; { ENABLE ParseError => GOTO parseErrorExit; fieldName: ROPE ¬ NIL; wantThisField, continue: BOOL ¬ TRUE; DO fieldName ¬ MailParse.GetFieldName[pH, NextChar]; IF fieldName = NIL THEN EXIT; IF proc # NIL THEN [wantThisField, continue] ¬ proc[fieldName]; IF wantThisField THEN msgHeaders ¬ CONS[[fieldName, MailParse.GetFieldBody[pH, NextChar]], msgHeaders] ELSE [] ¬ MailParse.GetFieldBody[pH, NextChar, TRUE]; IF ~continue THEN EXIT; ENDLOOP; MailParse.FinalizeParse[pH]; EXITS parseErrorExit => { MailParse.FinalizeParse[pH]; RETURN[msgHeaders]}; }; }; <> GetSendForm: PUBLIC PROC[fileName: ROPE] RETURNS[text: ViewerTools.TiogaContents] = { s: IO.STREAM; so: PFS.StreamOptions ¬ PFS.defaultStreamOptions; so[includeFormatting] ¬ TRUE; s ¬ PFS.StreamOpen[fileName: PFS.PathFromRope[fileName], streamOptions: so ! PFS.Error => { s ¬ NIL; CONTINUE }]; IF s # NIL THEN { text ¬ TiogaTextFromStrm[s ! PFS.Error => { text ¬ NIL; CONTINUE }]; s.Close[ ! PFS.Error => { s ¬ NIL; CONTINUE }] }; }; InternalDisplayTioga: PUBLIC PROC[ senderInfo: SenderInfo, tc: TiogaContents, grab: BOOL] = { <> senderV: Viewer ¬ senderInfo.senderV; iHadFocus: BOOL ¬ InputFocus.GetInputFocus[].owner = senderV; IF senderV.link # NIL THEN InternalDestroySplits[senderV]; IF TiogaOps.GetSelection[feedback].viewer = senderV THEN TiogaOps.CancelSelection[feedback]; ViewerTools.SetTiogaContents[senderV, tc]; -- test if I had the focus & no-one else has it now IF grab AND iHadFocus AND InputFocus.GetInputFocus[].owner = NIL THEN { ViewerTools.SetSelection[senderV, viewerStart]; [] ¬ TEditInputOps.DoFindPlaceholders[next: TRUE, gotoend: FALSE] }; UnsetNewVersion[senderV]; senderInfo.successfullySent ¬ FALSE; }; InsertForm: PUBLIC PROC[ sender: SenderInfo, form: SendMailOps.Form, force: BOOL ] = { senderV: Viewer = sender.senderV; whoHasIt: Viewer; IF senderV.iconic THEN { ViewerOps.AddProp[senderV, $SendMailOpsForm, form]; sender.openEvent ¬ ViewerEvents.RegisterEventProc[ proc: OpenSendViewer, event: open, filter: senderV, before: FALSE]; UnsetNewVersion[senderV]; -- not strictly true but want to reuse RETURN }; -- first stuff the text of the form into the Viewer InternalDisplayTioga[sender, form.formText, FALSE]; IF force OR ((whoHasIt ¬ InputFocus.GetInputFocus[].owner) = NIL) OR (whoHasIt = senderV) THEN DoPlaceHolders[senderV, form.fields] ELSE { ViewerOps.AddProp[senderV, $SendMailOpsFields, form.fields]; sender.focusEvent ¬ ViewerEvents.RegisterEventProc[ proc: SetFocusInSendViewer, event: setInputFocus, filter: senderV, before: FALSE]; UnsetNewVersion[senderV]; -- not strictly true but want to reuse }; }; OpenSendViewer: ViewerEvents.EventProc = { OPEN Menus; senderInfo: SenderInfo ¬ NARROW[ViewerOps.FetchProp[viewer, $SenderInfo]]; form: SendMailOps.Form; ra: REF ANY; IF senderInfo = NIL THEN RETURN[FALSE]; IF (ra ¬ ViewerOps.FetchProp[viewer, $SendMailOpsForm]) = NIL THEN RETURN[FALSE]; form ¬ NARROW[ra]; ViewerEvents.UnRegisterEventProc[senderInfo.openEvent, open]; senderInfo.openEvent ¬ NIL; TRUSTED { Process.Detach[FORK EntryInsertForm[senderInfo, form]]}; RETURN[FALSE]; }; SetFocusInSendViewer: ViewerEvents.EventProc = { OPEN Menus; senderInfo: SenderInfo ¬ NARROW[ViewerOps.FetchProp[viewer, $SenderInfo]]; fieldsList: LIST OF ROPE; ra: REF ANY; IF senderInfo = NIL THEN RETURN[FALSE]; IF (ra ¬ ViewerOps.FetchProp[viewer, $SendMailOpsFields]) = NIL THEN RETURN[FALSE]; fieldsList ¬ NARROW[ra]; ViewerEvents.UnRegisterEventProc[senderInfo.focusEvent, setInputFocus]; senderInfo.focusEvent ¬ NIL; TRUSTED { Process.Detach[FORK EntryPlaceHolders[senderInfo, fieldsList]]}; RETURN[FALSE]; }; DoPlaceHolders: PUBLIC PROC[senderV: Viewer, fieldsList: LIST OF ROPE] = { AddFields: PROC[ root: TiogaOps.Ref ] = { -- now try to find all of the placeholders in the text and match them with the entries in the fields list FOR rl: LIST OF ROPE ¬ fieldsList, rl.rest UNTIL rl = NIL DO field: ROPE = rl.first; IF NOT TEditInputOps.DoFindPlaceholders[next: TRUE, gotoend: FALSE].found THEN EXIT; IF field = NIL THEN LOOP; TiogaOps.Delete[]; TiogaOps.InsertRope[field] ENDLOOP }; IF senderV.destroyed OR senderV.iconic THEN RETURN; -- oops ViewerTools.SetSelection[ senderV, viewerStart]; TiogaOps.CallWithLocks[ AddFields ]; UnsetNewVersion[senderV]; ViewerTools.SetSelection[ senderV, viewerStart]; [] ¬ TEditInputOps.DoFindPlaceholders[next: TRUE, gotoend: FALSE]; }; Init: PROC ~ { smallBoldLooks['b] ¬ smallBoldLooks['s] ¬ TRUE; }; SendMailAnswerDebug: Commander.CommandProc = { sendMailDebug ¬ TRUE }; SendMailAnswerUndebug: Commander.CommandProc = { sendMailDebug ¬ FALSE }; Commander.Register["SendMailAnswerDebug", SendMailAnswerDebug]; Commander.Register["SendMailAnswerUndebug", SendMailAnswerUndebug]; END.