-- File:  AnswerImpl.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

DIRECTORY
  Answer USING [Block],
  Ascii USING [CR, SP],
  Inline USING [LongCOPY],
  MailParse USING [
    endOfInput, FinalizeParse, GetFieldBody, GetFieldName, InitializeParse,
    maxFieldNameSize,  maxRecipientLength, NameInfo, ParseError, ParseHandle,
    ParseNameList],
  Storage USING [Free, FreeString, Node, String],
  String USING [AppendChar, AppendString, EquivalentString, LowerCase];
  
AnswerImpl: PROGRAM
  IMPORTS Inline, MailParse, String, Storage EXPORTS Answer =
  
  BEGIN
  
  -- Local Data Structures and Types
  
  Buffer: TYPE = LONG STRING;
  CharIndex: TYPE = CARDINAL;
  nullIndex: CharIndex = LAST[CharIndex];
  
  Name: TYPE = POINTER TO NameRecord;
  NameRecord: TYPE = RECORD [simpleName, registry, arpaHost: STRING];
  
  Qualification: TYPE = {unqualified, dotQualified, arpaQualified};
  
  DuplicateNameHandle: TYPE = POINTER TO DuplicateName;
  DuplicateName: TYPE = RECORD [
    next: DuplicateNameHandle, seenOnSecondPass: BOOLEAN, name: STRING];
  
  -- Exported Procedures
  
  MakeHeader: PUBLIC PROCEDURE [
    getChar: PROCEDURE [CARDINAL] RETURNS [CHARACTER], getLength: CARDINAL,
    putBlock: PROCEDURE [Answer.Block],
    getPages: PROCEDURE [CARDINAL] RETURNS [LONG POINTER],
    freePages: PROCEDURE [LONG POINTER],
    userName, userRegistry: STRING,
    arpaGatewayHostNames: DESCRIPTOR FOR ARRAY OF STRING,
    cForCopies: BOOLEAN ← FALSE]
    RETURNS [answerError: BOOLEAN] =
    BEGIN
    getCharIndex, reStart: CharIndex;
    pH: MailParse.ParseHandle;
    havePH: BOOLEAN ← FALSE;
    buffer: Buffer ← NIL;
    idIndex: CharIndex ← nullIndex;
    dateIndex: CharIndex ← nullIndex;
    subjectIndex: CharIndex ← nullIndex;
    
    nameSize: CARDINAL = 60;
    
    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 STRING =
      ["Message-ID"L, "Reply-to"L, "Sender"L, "From"L, "To"L, "cc"L, "c"L, "bcc"L,
	"Date"L, "Subject"L]; -- order corresponds to FieldIndex
    answerTarget: FieldIndex ← SUCC[from];
    
    originSN: STRING = [MailParse.maxRecipientLength];
    originReg: STRING = [MailParse.maxRecipientLength];
    originH: STRING = [MailParse.maxRecipientLength];
    originPieces: NameRecord ← [originSN, originReg, originH];
    originName: Name = @originPieces;
    originQual: Qualification;
    originIndex: FieldIndex ← reply; -- anything except sender or from
    answerTargetBodyCharIndex: CharIndex ← 0;
    
    defaultRegistry: STRING = "PA"L;
    replyerRegIsDefault: BOOLEAN = String.EquivalentString[userRegistry, defaultRegistry];
    targetEqualsOrigin: BOOLEAN ← TRUE;
    
    namesOutput: BOOLEAN ← FALSE;
    replyerCCed: BOOLEAN ← FALSE;
    ccCount: CARDINAL ← 0;
    DuplicateHead: DuplicateNameHandle ← NIL;
    
    AppendToBuffer: PROCEDURE [b: POINTER TO Buffer, char: CHARACTER] =
      BEGIN
      IF b.length >= b.maxlength THEN {
	pages: CARDINAL = ((b.maxlength + 4)/512) + 1;
	temp: Buffer ← NewBuffer[pages];
	Inline.LongCOPY[from: @b.text, to: @temp.text, nwords: b.maxlength/2];
	temp.length ← b.length;
	freePages[b↑];
	b↑ ← temp};
      b[b.length] ← char;
      b.length ← b.length + 1;
      RETURN
      END; -- of AppendToBuffer --
      
    AppendStringToBuffer: PROCEDURE [b: POINTER TO Buffer, s: STRING] =
      BEGIN
      IF s # NIL THEN FOR i: CARDINAL IN [0..s.length) DO
	AppendToBuffer[b, s[i]] ENDLOOP;
      END; -- of AppendStringToBuffer --
      
    DeleteRangeInBuffer: PROCEDURE [b: Buffer, from, to: CARDINAL] =
      BEGIN
      dif: CARDINAL = to - from;
      b.length ← b.length - dif;
      FOR i: CARDINAL IN [from..b.length) DO b[i] ← b[i+dif] ENDLOOP;
      END; -- of DeleteRangeInBuffer --
      
    NewBuffer: PROCEDURE [pages: CARDINAL] RETURNS [b: Buffer] =
      BEGIN
      b ← getPages[pages];
      b↑ ← [length: 0, maxlength: pages*512 - 4, text: ];
      END; -- of NewBuffer --
      
    GetNextChar: PROCEDURE RETURNS [char: CHARACTER] =
      BEGIN
      char ←
	IF getCharIndex >= getLength THEN MailParse.endOfInput ELSE getChar[getCharIndex];
      getCharIndex ← getCharIndex + 1;
      END; -- of GetNextChar --
      
    BackupChar: PROCEDURE = {getCharIndex ← getCharIndex - 1};
    
    InitParse: PROCEDURE = {
	pH ← MailParse.InitializeParse[GetNextChar, BackupChar]; havePH ← TRUE};

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

    StuffChar: PROCEDURE [char: CHARACTER] =
      BEGIN
      IF char ~= Ascii.CR THEN [] ← AppendToBuffer[@buffer, char];
      END; -- of StuffChar --
      
    ProcessFields: PROCEDURE [Inner: PROC [index: FieldIndex]] =
      BEGIN OPEN MailParse;
      discardS: STRING = [0];
      fieldName: STRING = [maxFieldNameSize];
      getCharIndex ← 0;
      InitParse[];
      DO
	IF ~GetFieldName[pH, fieldName] THEN EXIT;
	FOR i: FieldIndex IN FieldIndex DO
	  IF String.EquivalentString[fieldName, knownField[i]] THEN {Inner[i]; EXIT};
	  REPEAT FINISHED => GetFieldBody[pH, discardS];
	  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: STRING, ignored: MailParse.NameInfo]
	RETURNS [accept: BOOLEAN] =
	BEGIN
	IF ~(originIndex = sender OR index = originIndex OR index = reply) THEN
	  BEGIN OPEN String;
	  originIndex ← index;
	  originName.simpleName.length ← originName.registry.length ←
	    originName.arpaHost.length ← 0;
	  AppendString[originName.simpleName, simpleName];
	  AppendString[originName.registry, registry];
	  AppendString[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 --
	
      MailParse.ParseNameList[pH, ProcessName];
      END; -- of AnalyzeOrigin --
      
    GetOrigin: PROCEDURE [index: FieldIndex] =
      BEGIN
      discard: STRING ← [0];
      SELECT index FROM
	IN [reply..from] => AnalyzeOrigin[index];
	ENDCASE => MailParse.GetFieldBody[pH, discard];
      RETURN
      END; -- of GetOrigin --

    FirstPass: PROCEDURE [index: FieldIndex] =
      BEGIN
      discard: STRING ← [0];
      SELECT index FROM
	id => {idIndex ← getCharIndex; MailParse.GetFieldBody[pH, discard]};
	IN [to..bcc] => FillNameField[firstPass: TRUE];
	date => {dateIndex ← getCharIndex; MailParse.GetFieldBody[pH, discard]};
	subject => {subjectIndex ← getCharIndex; MailParse.GetFieldBody[pH, discard]};
	ENDCASE => MailParse.GetFieldBody[pH, discard];
      RETURN
      END; -- of FirstPass --
      
    AppendMessageID: PROCEDURE =
      BEGIN
      StuffChar[',];
      StuffChar[Ascii.SP];
      FillField[];
      END; -- of AppendMessageID --
      
    LocalArpaSite: PROCEDURE [host: STRING] RETURNS [BOOLEAN] =
      BEGIN
      FOR i: CARDINAL IN [0..LENGTH[arpaGatewayHostNames]) DO
	IF String.EquivalentString[host, arpaGatewayHostNames[i]] 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: STRING;
	    SELECT originQual FROM
	      unqualified => senderRegistry ← userRegistry;
	      dotQualified => senderRegistry ← originName.registry;
	      arpaQualified => EXIT;
	      ENDCASE;
	    IF String.EquivalentString[name.registry, senderRegistry] THEN
	      BEGIN name.registry.length ← 0; qual ← unqualified; END;
	    EXIT
	    END;
	  arpaQualified =>
	    BEGIN
	    nameIsLocalArpa: BOOLEAN = LocalArpaSite[name.arpaHost];
	    IF nameIsLocalArpa AND name.registry.length = 0 AND
	      ~replyerRegIsDefault THEN
	      String.AppendString[name.registry, defaultRegistry];
	    IF originQual ~= arpaQualified THEN
	      IF nameIsLocalArpa THEN name.arpaHost.length ← 0 -- and LOOP
	      ELSE EXIT
	    ELSE
	      BEGIN
	      IF String.EquivalentString[name.arpaHost, originName.arpaHost] THEN {
		name.arpaHost.length ← 0; 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[0] FROM
		'@, '" => NULL;
		ENDCASE => String.AppendString[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.length ← 0;
	    newQual ← qual ← DetermineQualification[name];
	    IF qual = dotQualified THEN GO TO CheckEqualNA;
	    END;
	ENDCASE;
      EXITS
	AddOriginHost => String.AppendString[name.arpaHost, originName.arpaHost];
	CheckEqualNA =>
	  IF String.EquivalentString[name.registry, userRegistry] THEN
	    BEGIN newQual ← unqualified; name.registry.length ← 0; END;
      END; -- of AdjustToReplyerContext --
      
    FillField: PROCEDURE =
      BEGIN
      field: STRING = [124];
      MailParse.GetFieldBody[pH, field];
      IF field.length > field.maxlength - 4 THEN
	BEGIN
	field.length ← field.maxlength - 4;
	String.AppendString[field, " ..."L];
	END;
      AppendStringToBuffer[@buffer, field];
      END; -- of FillField --
      
    AddedToDuplicateList: PROCEDURE [name: Name, firstPass: BOOLEAN]
      RETURNS [added: BOOLEAN] =
      BEGIN
      item: DuplicateNameHandle;
      size: CARDINAL =
	name.simpleName.length +
	  (IF name.registry.length ~= 0 THEN name.registry.length + 1 ELSE 0) +
	  (IF name.arpaHost.length ~= 0 THEN name.arpaHost.length + 1 ELSE 0);
      s: STRING ← Storage.String[size];
      String.AppendString[s, name.simpleName];
      IF name.registry.length ~= 0 THEN
	{String.AppendChar[s, '.]; String.AppendString[s, name.registry]};
      IF name.arpaHost.length ~= 0 THEN
	{String.AppendChar[s, '@]; String.AppendString[s, name.arpaHost]};
      FOR item ← DuplicateHead, item.next UNTIL item = NIL DO
	IF String.EquivalentString[item.name, s] THEN
	  BEGIN
	  Storage.FreeString[s];
	  IF firstPass THEN RETURN[FALSE];
	  added ← ~item.seenOnSecondPass;
	  item.seenOnSecondPass ← TRUE;
	  RETURN
	  END;
	ENDLOOP;
      item ← Storage.Node[SIZE[DuplicateName]];
      item.name ← s;
      item.seenOnSecondPass ← FALSE;
      item.next ← DuplicateHead;
      DuplicateHead ← item;
      RETURN[TRUE]
      END; -- of AddedToDuplicateList --
      
    ReleaseDuplicateList: PROCEDURE =
      BEGIN
      item: DuplicateNameHandle;
      UNTIL DuplicateHead = NIL DO
	item ← DuplicateHead.next;
	Storage.FreeString[DuplicateHead.name];
	Storage.Free[DuplicateHead];
	DuplicateHead ← item;
	ENDLOOP;
      END; -- of ReleaseDuplicateList --
      
    ProcessAnswerTarget: PROCEDURE =
      BEGIN
      
      ProcessName: PROCEDURE [
	simpleName, registry, arpaHost: STRING, ignored: MailParse.NameInfo]
	RETURNS [accept: BOOLEAN] =
	BEGIN
	name: NameRecord ← [simpleName, registry, arpaHost];
	qual: Qualification ← AdjustToSenderContext[@name];
	[] ← AddedToDuplicateList[@name, FALSE];
	[] ← AdjustToReplyerContext[@name, qual];
	RETURN[TRUE];
	END; -- of ProcessName --
	
      getCharIndex ← answerTargetBodyCharIndex;
      InitParse[];
      MailParse.ParseNameList[pH, ProcessName, StuffChar, TRUE];
      FinParse[];
      END; -- of ProcessAnswerTarget --
      
    AnalyzeAnswerTarget: PROCEDURE =
      BEGIN
      
      ProcessName: PROCEDURE [
	simpleName, registry, arpaHost: STRING, ignored: MailParse.NameInfo]
	RETURNS [accept: BOOLEAN] =
	BEGIN
	name: NameRecord ← [simpleName, registry, arpaHost];
	qual: Qualification ← AdjustToSenderContext[@name];
        targetEqualsOrigin ← targetEqualsOrigin AND
	  String.EquivalentString[simpleName, originName.simpleName] AND
	  String.EquivalentString[registry, originName.registry] AND
	  String.EquivalentString[arpaHost, originName.arpaHost];
	IF ~AddedToDuplicateList[@name, TRUE] THEN ccCount ← ccCount - 1;
	RETURN[FALSE];
	END; -- of ProcessName --
	
      IF answerTargetBodyCharIndex = 0 THEN
	ERROR MailParse.ParseError[badFieldName];
      getCharIndex ← answerTargetBodyCharIndex;
      InitParse[];
      MailParse.ParseNameList[pH, ProcessName, !
	MailParse.ParseError => answerError ← TRUE];
      FinParse[];
      END; -- of AnalyzeAnswerTarget --
      
    FillNameField: PROCEDURE [firstPass: BOOLEAN] =
      BEGIN
      firstOutput: BOOLEAN ← TRUE;
      
      ProcessName: PROCEDURE [
	simpleName, registry, arpaHost: STRING, nameInfo: MailParse.NameInfo]
	RETURNS [accept: BOOLEAN] =
	BEGIN
	name: 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 String.EquivalentString[simpleName, userName] AND
	    registry.length = 0 AND arpaHost.length = 0 THEN replyerCCed ← TRUE;
	  RETURN[FALSE]
	  END;
	IF firstOutput THEN
	  BEGIN
	  firstOutput ← FALSE;
	  IF namesOutput THEN {StuffChar[',]; StuffChar[Ascii.SP]}
	  END;
	RETURN[namesOutput ← TRUE]
	END; -- of ProcessName --
	
      MailParse.ParseNameList[pH, ProcessName, StuffChar, TRUE];
      END; -- of FillNameField --
      
    SecondPass: PROCEDURE [index: FieldIndex] =
      BEGIN
      discard: STRING = [0];
      SELECT index FROM
	IN [to..bcc] => FillNameField[firstPass: FALSE];
	ENDCASE => MailParse.GetFieldBody[pH, discard];
      END; -- of SecondPass --
      
    PutBuffer: PROCEDURE =
      BEGIN
      AppendToBuffer[@buffer, Ascii.CR];
      putBlock[[@buffer.text, buffer.length]];
      buffer.length ← 0;
      END; -- of PutBuffer --
      
  -- main body of AnswerCommand
  
    answerError ← FALSE;
    buffer ← NewBuffer[1];
    
    BEGIN
    
    -- find out who it's from and where the interesting indices are
    ProcessFields[GetOrigin ! MailParse.ParseError => GO TO BadMessage];
    ProcessFields[FirstPass ! MailParse.ParseError => GO TO BadMessage];
    AnalyzeAnswerTarget[ ! MailParse.ParseError => GO TO BadMessage];
    
    -- make Subject field
    AppendStringToBuffer[@buffer, "Subject: Re: "L];
    IF subjectIndex # nullIndex THEN {
      getCharIndex ← subjectIndex; InitParse[]; FillField[]; FinParse[]};
    reStart ← 13;
    WHILE (buffer.length > reStart + 2) AND
      (String.LowerCase[buffer[reStart]] = 'r) AND
      (String.LowerCase[buffer[reStart+1]] = 'e) AND
      (buffer[reStart+2] = ':) DO
      DeleteRangeInBuffer[buffer, reStart, reStart + 3];
      WHILE buffer.length > reStart AND buffer[reStart] = Ascii.SP DO
	DeleteRangeInBuffer[buffer, reStart, reStart + 1]; ENDLOOP;
      ENDLOOP;
    PutBuffer[];
      
    -- make In-reply-to field
    AppendStringToBuffer[@buffer, "In-reply-to: "L];
    IF (IF answerTarget = reply THEN targetEqualsOrigin
       ELSE (ccCount = 0 OR (replyerCCed AND ccCount = 1))) THEN
      AppendStringToBuffer[@buffer, "Your"L]
    ELSE
      BEGIN
      AppendStringToBuffer[@buffer, originSN];
      IF originReg.length ~= 0 THEN
	BEGIN
	AppendToBuffer[@buffer, '.];
	AppendStringToBuffer[@buffer, originReg];
	END;
      AppendToBuffer[@buffer, ''];
      IF originReg.length ~= 0 OR String.LowerCase[originSN[originSN.length - 1]] ~= 's
	THEN AppendToBuffer[@buffer, 's];
      END;
    AppendStringToBuffer[@buffer, " message of "L];
    InitParse[];
    IF dateIndex # nullIndex THEN {getCharIndex ← dateIndex; FillField[]};
    IF idIndex # nullIndex THEN {getCharIndex ← idIndex; AppendMessageID[]};
    FinParse[];
    PutBuffer[];
    
    -- fill in target (To:) field of answer form
    AppendStringToBuffer[@buffer, "To: "L];
    ProcessAnswerTarget[];
    PutBuffer[];
    
    -- fill in cc: field
    AppendStringToBuffer[@buffer, IF cForCopies THEN "c:  "L ELSE "cc: "L];
    IF answerTarget = reply THEN AppendStringToBuffer[@buffer, userName]
    ELSE ProcessFields[SecondPass ! MailParse.ParseError => GO TO BadMessage];
    PutBuffer[];
    
    -- empty line at end of header
    PutBuffer[];
    
    EXITS BadMessage => {IF havePH THEN FinParse[]; answerError ← TRUE};
    END;
    
    freePages[buffer];
    ReleaseDuplicateList[];
    
    RETURN[answerError]
    END; -- of MakeHeader --
    
    
  END.  -- AnswerImpl --