-- File: WalnutSendMailImpl.mesa
-- Contents: Implementation of the WalnutMsg Send operation
-- Last Edited by: Willie-Sue, December 7, 1982 3:30 pm
-- Rick on: XXX
-- Willie-Sue on: June 2, 1983 12:41 pm

DIRECTORY
GVBasics USING [ItemType, RName],
GVMailParse,
GVNames,
GVSend USING [Abort, AddRecipient, AddToItem, CheckValidity, Close, Create, Handle,
  Send, SendFailed, StartItem, StartSend, StartSendInfo],
Icons,
List USING [Append],
Menus,
IO,
Rope,
TEditDocumentRope USING[ Create ],
TiogaOps,
ViewerOps,
ViewerTools,
ViewerClasses,
UserCredentials USING [GetUserCredentials],
WalnutSendMail,
WalnutParse;

WalnutSendMailImpl: CEDAR MONITOR
IMPORTS
GVMailParse, GVNames, GVSend, List, IO, Rope,
TEditDocumentRope, TiogaOps,
ViewerOps, ViewerTools,
UserCredentials,
WalnutSendMail
EXPORTS
WalnutSendMail, WalnutParse =

BEGIN OPEN WalnutSendMail, WalnutParse;

-- Global types & variables

defaultRegistry: PUBLIC ROPE← "pa";

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]
 ];

-- ************************************************************************
SendMsg: PUBLIC PROC[senderV: Viewer, doClose: BOOLFALSE] RETURNS[sendOk: BOOL] =
{ senderV.inhibitDestroy← TRUE;
ViewerOps.SetMenu[senderV, sendingMenu];
IF needToAuthenticate THEN
  { SenderReport["Authenticating user ..."];
IF ~AuthenticateUser[] THEN
  {ViewerOps.BlinkIcon[senderV, IF senderV.iconic THEN 0 ELSE 1];
  ViewerOps.SetMenu[senderV, sendMenu];
  senderV.inhibitDestroy← FALSE;
RETURN
};
  SenderReport[" ...ok\n"];
  };
  sendOk← InternalSendMsg[senderV, doClose];
ViewerOps.SetMenu[senderV, sendMenu];
senderV.inhibitDestroy← FALSE;
};
-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

AuthenticateUser
: PROC RETURNS [BOOL] =
BEGIN
auth: GVNames.AuthenticateInfo;
IF userRName.Length[] = 0 THEN {SenderReport["Please Login\n"]; RETURN[FALSE]};

SELECT
auth←
GVNames.Authenticate[userRName, UserCredentials.GetUserCredentials[].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;
specialTxt, formatting: ROPE;
smr: SendingRec;
contents: ViewerTools.TiogaContents;
senderInfo: SenderInfo← NARROW[ViewerOps.FetchProp[senderV, $SenderInfo]];

sendOk← FALSE;
senderInfo.aborted← FALSE;
-- senderV.inhibitDestroy← TRUE;
-- ViewerOps.SetMenu[senderV, sendingMenu];

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←
TEditDocumentRope.Create[LOOPHOLE [TiogaOps.ViewerDoc[senderV]]]};

[status, sPos, mPos]← ParseTextToBeSent[smr];

IF (status # ok) AND (status # includesPublicDL) THEN
BEGIN
SELECT status FROM
  fieldNotAllowed =>
IF sPos # mPos THEN
  { 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
  { 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;
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];
};
IF howToReply # all THEN
  InsertIntoViewer[senderV,
    Rope.Concat["\nReply-To: ", userRName], smr.endHeadersPos-2];  --you dont't want to know why
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];

 specialTxt← Rope.Cat["Date: ", IO.PutFR[NIL, IO.time[]],
  IF smr.from = NIL THEN "\nFrom: " ELSE "\nSender: ", userRName, "\n"];
 InsertIntoViewer[senderV, specialTxt, 0];

 contents← ViewerTools.GetTiogaContents[senderV];
IF (formatting← contents.formatting).Length[] = 0 THEN smr.fullText← contents.contents
ELSE
  { 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]
  };

SenderReport["... Sending message..."];
IF senderInfo.successfullySent← sendOk← Send[smr, senderInfo, formatting]
THEN
{SenderReport[" ... Message has been delivered\n"];
senderInfo.prevMsg← contents;
senderInfo.numCharsToDelete← specialTxt.Length[] - 1;
}
ELSE
{DeleteChars[senderV, specialTxt.Length[] - 1];
  SenderReport["... Message NOT sent\n"];
  };
END;

InsertIntoViewer: PUBLIC PROC[v: Viewer, what: ROPE, where: INT] =
BEGIN OPEN TiogaOps;
 thisV: Ref← ViewerDoc[v];
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];
  TiogaOps.InsertRope[what];
  ViewerTools.InhibitUserEdits[v];
  IF (prevV # v) AND (prevV#NIL) THEN
   SetSelection[prevV, prevStart, prevEnd, prevLevel, cb, pd];
END;
 CallWithLocks[InsertChars, thisV];
END;

DeleteChars: PUBLIC ENTRY PROC[v: Viewer, num: INT] =
BEGIN ENABLE UNWIND => NULL;
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[];
  IF (prevV # v) AND (prevV#NIL) THEN
   SetSelection[prevV, prevStart, prevEnd, prevLevel, cb, pd];
END;
  CallWithLocks[DelChars, thisV];
END;

-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Send: 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;
IF
senderInfo.sendHandle#NIL THEN GVSend.Close[senderInfo.sendHandle];
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: BOOLTRUE;
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.GetUserCredentials[].password,
     sender: userRName,
     returnTo: 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 ~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 ~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, TiogaCTRL]; GVSend.AddToItem[h, formatting] };
 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: MessageInfo;
tHeaders: LIST OF ROPENIL;
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: 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: ROPENIL;
fieldNotRecognized: BOOL;
mPos← 0;  -- where we are in the fulltext, for parsing
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: MessageFieldIndex IN MessageFieldIndex DO
{ mLF← messageParseArray[i];
IF Rope.Equal[messageParseArray[i].name, field, FALSE] THEN-- ignore case
{ fieldNotRecognized← FALSE;
SELECT mLF.fType FROM
   simpleRope =>
    IF i = fromF THEN RNameListField[i ! ParseError => GOTO errorExit] ELSE
    {IF i = replyToF THEN msg.replyTo← TRUE;
     []← 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;

-- now we are positioned at the beginning of the body of the message
FinalizeParse[pH];
msg.endHeadersPos← mPos - 1;

-- if any recipient is at another arpa site, all recipients should be arpa qualified
-- however, like Laurel, we'll only do the From/Sender field in full arpa regalia

 msg.numRecipients← countOfRecipients;
 msg.numDLs← dlCount;
EXITS
 errorExit => RETURN[syntaxError, sPos, mPos];
END;

END.