-- File: GVAnswerImpl.Mesa
-- based on [Juniper]<DMS>Laurel>6T30>IntAnswerCom.mesa
-- edited by Karlton, Friday Feb. 13, 1981 6:10 pm PST
-- edited by Levin, 29-Mar-82 9:34:23

-- edited by Willie-Sue on: January 21, 1983 10:43 am

DIRECTORY
Rope,
GVAnswer,
GVMailParse USING [
endOfInput, FinalizeParse, GetFieldBody, GetFieldName, InitializeParse,
NameInfo, ParseError, ParseHandle, ParseNameList];

GVAnswerImpl: CEDAR PROGRAM
IMPORTS GVMailParse, Rope
EXPORTS GVAnswer =

BEGIN

-- Local Data Structures and Types

ROPE: TYPE = Rope.ROPE;
CharIndex: TYPE = INT;
nullIndex: CharIndex = LAST[CharIndex];

Name: TYPE = REF NameRecord;
NameRecord: TYPE = RECORD [simpleName, registry, arpaHost: ROPE];

Qualification: TYPE = {unqualified, dotQualified, arpaQualified};

DuplicateNameHandle: TYPE = REF DuplicateName;
DuplicateName: TYPE = RECORD [
next: DuplicateNameHandle, seenOnSecondPass: BOOLEAN, name: ROPE];

-- Exported Procedures

MakeHeader: PUBLIC PROCEDURE [
getChar: PROCEDURE [INT] RETURNS [CHARACTER],
getLength: INT,
userName, userRegistry: ROPE,
arpaGatewayHostNames: LIST OF ROPE,
cForCopies: BOOLEANFALSE]
   RETURNS [answerError: BOOLEAN, answer: ROPE] =
BEGIN
getCharIndex, reStart: CharIndex;
pH: GVMailParse.ParseHandle;
havePH: BOOLEANFALSE;
idIndex: CharIndex ← nullIndex;
dateIndex: CharIndex ← nullIndex;
subjectIndex: CharIndex ← nullIndex;

 rp: ROPE; rLen: INT;

PutIndex: TYPE = {subject, inreply, to, cc}; -- order is significant!
FieldIndex: TYPE = {
id, reply, sender, from, to, cc, c, bcc, date, subject}; -- order is significant!
knownField: ARRAY FieldIndex OF ROPE =
["Message-ID", "Reply-to", "Sender", "From", "To", "cc", "c", "bcc",
 "Date", "Subject"]; -- order corresponds to FieldIndex
answerTarget: FieldIndex ← SUCC[from];

originName: Name = NEW[NameRecord← [NIL, NIL, NIL]];
originQual: Qualification;
originIndex: FieldIndex ← reply; -- anything except sender or from
answerTargetBodyCharIndex: CharIndex ← 0;

defaultRegistry: ROPE = "pa";
replyerRegIsDefault: BOOLEAN = Rope.Equal[userRegistry, defaultRegistry, FALSE];
targetEqualsOrigin: BOOLEANTRUE;

namesOutput: BOOLEANFALSE;
replyerCCed: BOOLEANFALSE;
ccCount: CARDINAL ← 0;
DuplicateHead: DuplicateNameHandle ← NIL;

GetNextChar: PROCEDURE RETURNS [char: CHARACTER] =
BEGIN
char ←
  IF getCharIndex >= getLength THEN GVMailParse.endOfInput ELSE getChar[getCharIndex];
getCharIndex ← getCharIndex + 1;
END; -- of GetNextChar --

BackupChar: PROCEDURE = {getCharIndex ← getCharIndex - 1};

InitParse: PROCEDURE = {
 pH ← GVMailParse.InitializeParse[GetNextChar, BackupChar]; havePH ← TRUE};

FinParse: PROCEDURE = {GVMailParse.FinalizeParse[pH]; havePH ← FALSE};

ProcessFields: PROCEDURE [Inner: PROC [index: FieldIndex]] =
BEGIN OPEN GVMailParse;
fieldName: ROPE;
  found: BOOL;
getCharIndex ← 0;
InitParse[];
DO
  [found, fieldName]← GetFieldName[pH];
  IF ~found THEN EXIT;
  FOR i: FieldIndex IN FieldIndex DO
    IF Rope.Equal[fieldName, knownField[i], FALSE] THEN {Inner[i]; EXIT};
   REPEAT FINISHED => []← GetFieldBody[pH];
   ENDLOOP;
  ENDLOOP;
FinParse[];
END; -- of ProcessFields --

DetermineQualification: PROCEDURE [name: Name] RETURNS [Qualification] =
BEGIN
RETURN[
  SELECT TRUE FROM
   name.arpaHost.Length[] > 0 => arpaQualified,
   name.registry.Length[] > 0 => dotQualified,
   ENDCASE => unqualified]
END; -- of DetermineQualification --

AnalyzeOrigin: PROCEDURE [index: FieldIndex] =
BEGIN
fieldBodyStartIndex: CharIndex = getCharIndex;

ProcessName: PROCEDURE [
  simpleName, registry, arpaHost: ROPE, ignored: GVMailParse.NameInfo]
  RETURNS [accept: BOOLEAN] =
  BEGIN
  IF ~(originIndex = sender OR index = originIndex OR index = reply) THEN
   BEGIN
   originIndex ← index;
   originName.simpleName← simpleName;
   originName.registry← registry;
   originName.arpaHost← arpaHost;
   originQual ← DetermineQualification[originName];
   originQual ← AdjustToReplyerContext[originName, originQual];
   END;
  IF index < answerTarget AND
   ~(originQual = arpaQualified AND index = sender) THEN
   BEGIN
   answerTarget ← index;
   answerTargetBodyCharIndex ← fieldBodyStartIndex;
   END;
  RETURN[FALSE]
  END; -- of ProcessName --

GVMailParse.ParseNameList[pH, ProcessName];
END; -- of AnalyzeOrigin --

GetOrigin: PROCEDURE [index: FieldIndex] =
BEGIN
SELECT index FROM
  IN [reply..from] => AnalyzeOrigin[index];
  ENDCASE => []← GVMailParse.GetFieldBody[pH];
RETURN
END; -- of GetOrigin --

FirstPass: PROCEDURE [index: FieldIndex] =
BEGIN
SELECT index FROM
  id => {idIndex ← getCharIndex; []← GVMailParse.GetFieldBody[pH]};
  IN [to..bcc] => FillNameField[firstPass: TRUE];
  date => {dateIndex ← getCharIndex; []← GVMailParse.GetFieldBody[pH]};
  subject => {subjectIndex ← getCharIndex; []← GVMailParse.GetFieldBody[pH]};
  ENDCASE => []← GVMailParse.GetFieldBody[pH];
RETURN
END; -- of FirstPass --

LocalArpaSite: PROCEDURE [host: ROPE] RETURNS [BOOLEAN] =
BEGIN
FOR l: LIST OF ROPE← arpaGatewayHostNames, l.rest UNTIL l=NIL DO
  IF Rope.Equal[host, l.first, FALSE] THEN RETURN[TRUE];
  ENDLOOP;
RETURN[FALSE]
END; -- of LocalArpaSite --

AdjustToSenderContext: PROCEDURE [name: Name] RETURNS [qual: Qualification] =
BEGIN
DO
-- loops only if name has local Arpa qualification and originator is local as well.
  SELECT qual ← DetermineQualification[name] FROM
   unqualified => EXIT;
   dotQualified =>
   BEGIN
   senderRegistry: ROPE;
   SELECT originQual FROM
   unqualified => senderRegistry ← userRegistry;
   dotQualified => senderRegistry ← originName.registry;
   arpaQualified => EXIT;
   ENDCASE;
   IF Rope.Equal[name.registry, senderRegistry, FALSE] THEN
   BEGIN name.registry← NIL; qual ← unqualified; END;
   EXIT
   END;
   arpaQualified =>
   BEGIN
   nameIsLocalArpa: BOOLEAN = LocalArpaSite[name.arpaHost];
   IF nameIsLocalArpa AND name.registry.Length[] = 0 AND
   ~replyerRegIsDefault THEN name.registry← defaultRegistry;
   IF originQual ~= arpaQualified THEN
   IF nameIsLocalArpa THEN name.arpaHost← NIL -- and LOOP
   ELSE EXIT
   ELSE
   BEGIN
   IF Rope.Equal[name.arpaHost, originName.arpaHost, FALSE] THEN {
    name.arpaHost← NIL; qual ← DetermineQualification[name]};
   EXIT
   END;
   END;
   ENDCASE;
  ENDLOOP;
END; -- of AdjustToSenderContext --

AdjustToReplyerContext: PROCEDURE [name: Name, qual: Qualification]
RETURNS [newQual: Qualification] =
BEGIN
SELECT newQual ← qual FROM
  unqualified =>
   SELECT newQual ← originQual FROM
   dotQualified =>
   SELECT name.simpleName.Fetch[0] FROM
    '@, '" => NULL;
    ENDCASE => name.registry← originName.registry;
   arpaQualified => GO TO AddOriginHost;
   ENDCASE;
  dotQualified =>
   SELECT originQual FROM
   unqualified, dotQualified => GO TO CheckEqualNA;
   arpaQualified => BEGIN newQual ← originQual; GO TO AddOriginHost END;
   ENDCASE;
  arpaQualified =>
   IF LocalArpaSite[name.arpaHost] THEN
   BEGIN
   name.arpaHost← NIL;
   newQual ← qual ← DetermineQualification[name];
   IF qual = dotQualified THEN GO TO CheckEqualNA;
   END;
  ENDCASE;
EXITS
 AddOriginHost => name.arpaHost← originName.arpaHost;
 CheckEqualNA =>
IF Rope.Equal[name.registry, userRegistry, FALSE] THEN
BEGIN newQual ← unqualified; name.registry← NIL; END;
END; -- of AdjustToReplyerContext --

FillField: PROC RETURNS[field: ROPE] =
BEGIN
-- field: STRING = [124];
field← GVMailParse.GetFieldBody[pH];
IF field.Length[] > 124 - 4 THEN
  field← Rope.Concat[Rope.Substr[field, 0, 120], " ..."];
END; -- of FillField --

AddedToDuplicateList: PROCEDURE [name: Name, firstPass: BOOLEAN]
RETURNS [added: BOOLEAN] =
BEGIN
item: DuplicateNameHandle;
  rp: ROPE← name.simpleName;
IF name.registry.Length[] # 0 THEN rp← Rope.Cat[rp, ".", name.registry];
IF name.arpaHost.Length[] # 0 THEN rp← Rope.Cat[rp, "@", name.arpaHost];
FOR item ← DuplicateHead, item.next UNTIL item = NIL DO
  IF Rope.Equal[item.name, rp, FALSE] THEN
   BEGIN
   IF firstPass THEN RETURN[FALSE];
   added ← ~item.seenOnSecondPass;
   item.seenOnSecondPass ← TRUE;
   RETURN
   END;
  ENDLOOP;
DuplicateHead←
   NEW[DuplicateName← [name: rp, seenOnSecondPass: FALSE, next: DuplicateHead]];
RETURN[TRUE]
END; -- of AddedToDuplicateList --

ProcessAnswerTarget: PROCEDURE =
BEGIN

ProcessName: PROCEDURE [
  simpleName, registry, arpaHost: ROPE, ignored: GVMailParse.NameInfo]
  RETURNS [accept: BOOLEAN] =
  BEGIN
  name: REF NameRecord← NEW[NameRecord ← [simpleName, registry, arpaHost]];
  qual: Qualification ← AdjustToSenderContext[name];
  [] ← AddedToDuplicateList[name, FALSE];
  [] ← AdjustToReplyerContext[name, qual];
  RETURN[TRUE];
  END; -- of ProcessName --
  rp: ROPE← "";
AccumulateName: PROC[ch: CHAR] =
  { IF ch#'\n THEN rp← Rope.Concat[rp, Rope.FromChar[ch]]};

getCharIndex ← answerTargetBodyCharIndex;
InitParse[];
GVMailParse.ParseNameList[pH, ProcessName, AccumulateName, TRUE];
FinParse[];
IF rp.Length[]#0 THEN answer← Rope.Concat[answer, Rope.Flatten[rp]];
END; -- of ProcessAnswerTarget --

AnalyzeAnswerTarget: PROCEDURE =
BEGIN

ProcessName: PROCEDURE [
  simpleName, registry, arpaHost: ROPE, ignored: GVMailParse.NameInfo]
  RETURNS [accept: BOOLEAN] =
  BEGIN
  name: REF NameRecord← NEW[NameRecord← [simpleName, registry, arpaHost]];
  qual: Qualification ← AdjustToSenderContext[name];
  targetEqualsOrigin ← targetEqualsOrigin AND
    Rope.Equal[simpleName, originName.simpleName, FALSE] AND
    Rope.Equal[registry, originName.registry, FALSE] AND
    Rope.Equal[arpaHost, originName.arpaHost, FALSE];
  IF ~AddedToDuplicateList[name, TRUE] THEN ccCount ← ccCount - 1;
  RETURN[FALSE];
  END; -- of ProcessName --

IF answerTargetBodyCharIndex = 0 THEN ERROR GVMailParse.ParseError[badFieldName];
getCharIndex ← answerTargetBodyCharIndex;
InitParse[];
GVMailParse.ParseNameList[pH, ProcessName, !
    GVMailParse.ParseError => answerError ← TRUE];
FinParse[];
END; -- of AnalyzeAnswerTarget --

FillNameField: PROCEDURE [firstPass: BOOLEAN] =
BEGIN
firstOutput: BOOLEANTRUE;
  rp: ROPE← "";

ProcessName: PROCEDURE [
  simpleName, registry, arpaHost: ROPE, nameInfo: GVMailParse.NameInfo]
   RETURNS [accept: BOOLEAN] =
  BEGIN
  name: REF NameRecord ← NEW[NameRecord← [simpleName, registry, arpaHost]];
  qual: Qualification ← AdjustToSenderContext[name];
  new: BOOLEAN ← AddedToDuplicateList[name, firstPass];
  IF nameInfo.nesting = none AND ~new THEN RETURN[FALSE];
  [] ← AdjustToReplyerContext[name, qual];
  IF firstPass THEN
   BEGIN
   ccCount ← ccCount + 1;
   IF ~replyerCCed AND Rope.Equal[simpleName, userName, FALSE] AND
   registry.Length[] = 0 AND arpaHost.Length[] = 0 THEN replyerCCed ← TRUE;
   RETURN[FALSE]
   END;
  IF firstOutput THEN
   BEGIN
   firstOutput ← FALSE;
   IF namesOutput THEN rp← Rope.Concat[rp, ", "];
   END;
  RETURN[namesOutput ← TRUE]
  END; -- of ProcessName --

  AccumulateChars: PROC[ch: CHAR] =
  { IF ch#'\n THEN rp← Rope.Concat[rp, Rope.FromChar[ch]]};

GVMailParse.ParseNameList[pH, ProcessName, AccumulateChars, TRUE];
IF rp.Length[]#0 THEN answer← Rope.Concat[answer, Rope.Flatten[rp]];
END; -- of FillNameField --

SecondPass: PROCEDURE [index: FieldIndex] =
BEGIN
SELECT index FROM
  IN [to..bcc] => FillNameField[firstPass: FALSE];
  ENDCASE => []← GVMailParse.GetFieldBody[pH];
END; -- of SecondPass --

-- main body of AnswerCommand

answerError ← FALSE;
-- answer: ROPE;

BEGIN

-- find out who it's from and where the interesting indices are
ProcessFields[GetOrigin ! GVMailParse.ParseError => GO TO BadMessage];
ProcessFields[FirstPass ! GVMailParse.ParseError => GO TO BadMessage];
AnalyzeAnswerTarget[ ! GVMailParse.ParseError => GO TO BadMessage];

-- make Subject field
answer← "Subject: Re: ";
IF subjectIndex # nullIndex THEN {
getCharIndex ← subjectIndex; InitParse[]; rp← FillField[]; FinParse[]};
reStart ← 0;
 rLen← rp.Length[];
WHILE (rLen > reStart + 2) AND (Rope.Lower[rp.Fetch[reStart]] = 'r) AND
(Rope.Lower[rp.Fetch[reStart+1]] = 'e) AND (rp.Fetch[reStart+2] = ':)
  DO reStart← reStart + 3;
WHILE rLen > reStart AND rp.Fetch[reStart] = ' DO reStart← reStart + 1; ENDLOOP;
ENDLOOP;
answer← Rope.Concat[answer, Rope.Substr[rp, reStart, rLen-reStart]];

-- make In-reply-to field
answer← Rope.Concat[answer, "\nIn-reply-to: "];
IF (IF answerTarget = reply THEN targetEqualsOrigin
ELSE (ccCount = 0 OR (replyerCCed AND ccCount = 1))) THEN
answer← Rope.Concat[answer, "Your"]
ELSE
BEGIN
answer← Rope.Concat[answer, originName.simpleName];
IF originName.registry.Length # 0 THEN
  answer← Rope.Cat[answer, ".", originName.registry];
answer← Rope.Concat[answer, "'"];
IF originName.registry.Length # 0 OR
   Rope.Lower[originName.simpleName.Fetch[originName.simpleName.Length[]-1]] # 's
   THEN answer← Rope.Concat[answer, "s"];
END;
answer← Rope.Concat[answer, " message of "];
InitParse[];
IF dateIndex # nullIndex THEN
  {getCharIndex ← dateIndex; answer← Rope.Concat[answer, FillField[]]};
IF idIndex # nullIndex THEN
  {getCharIndex ← idIndex; answer← Rope.Cat[answer, ", ", FillField[]]};
FinParse[];

-- fill in target (To:) field of answer form
answer← Rope.Concat[answer, "\nTo: "];
ProcessAnswerTarget[];

-- fill in cc: field
answer← Rope.Concat[answer, IF cForCopies THEN "\nc: " ELSE "\ncc: "];
IF answerTarget = reply THEN answer← Rope.Concat[answer, userName]
  ELSE ProcessFields[SecondPass ! GVMailParse.ParseError => GO TO BadMessage];

-- empty line at end of header
answer← Rope.Concat[answer, "\n"];

EXITS BadMessage => {IF havePH THEN FinParse[]; answerError ← TRUE};
END;

RETURN[answerError, answer]
END; -- of MakeHeader --

END. -- GVAnswerImpl --

Edit Log.
WSH on November 22, 1982; get rid of dependency on IO