ArpaSMTPSyntaxImpl.mesa
Copyright © 1985 by Xerox Corporation. All rights reserved.
Last Edited by: HGM, May 8, 1985 0:30:18 am PDT
Last Edited by: DCraft, November 22, 1983 1:59 pm
Last Edited by: Taft, February 3, 1984 1:17:33 pm PST
Hal Murray July 2, 1985 2:31:00 am PDT
John Larson, July 19, 1988 3:01:41 pm PDT
DIRECTORY
ArpaConfig USING [bitnetGateway, uucpGateway, csnetGateway, ourLocalName, ourLocalAddress, specialDomains, validDomains, resolv],
ArpaName USING [AliasToName],
ArpaSMTPControl USING [defaultRegistry],
ArpaSMTPSupport USING [CreateSubrangeStream, Log],
ArpaSMTPSyntax USING [GVItemProc],
Basics USING [bytesPerWord],
ConvertExtras USING [RopeFromArpaAddress],
GVBasics USING [ItemHeader, RopeFromTimestamp, Timestamp],
GVProtocol USING [Failed, ReceiveCount, ReceiveItemHeader, ReceiveRName, ReceiveTimestamp],
IO USING [EndOfStream, GetBlock, GetIndex, PutBlock, PutChar, PutF, PutRope, SetIndex, STREAM],
RefText USING [AppendChar, ObtainScratch, ReleaseScratch],
Rope USING [Cat, Concat, Equal, Fetch, Find, FromChar, FromRefText, IsEmpty, Length, ROPE, Substr, Translate];
ArpaSMTPSyntaxImpl: CEDAR PROGRAM
IMPORTS ArpaConfig, ArpaName, ArpaSMTPControl, ArpaSMTPSupport, ConvertExtras, GVBasics, GVProtocol, IO, RefText, Rope
EXPORTS ArpaSMTPSyntax =
BEGIN
STREAM: TYPE = IO.STREAM;
ROPE: TYPE = Rope.ROPE;
possibleOldNames: LIST OF ROPELIST["Xerox.ARPA", "[10.2.0.32].ARPA"];
possibleCurrentNames: LIST OF ROPELIST["Xerox.COM", "PARC.Xerox.COM", "[13.0.12.232].ARPA", "mailhost.Xerox.COM", "mailhost.parc.Xerox.COM"];
arpaSpecialRegistries: LIST OF ROPELIST["AG", "ArpaGateway", "NotArpa"];
EnumerateGVItems: PUBLIC PROC [GVStream: STREAM, proc: ArpaSMTPSyntax.GVItemProc,
procData: REF ANYNIL] = {
nextItemIndex: INT;
continue: BOOL;
itemHeader: GVBasics.ItemHeader;
DO
itemHeader ← GVProtocol.ReceiveItemHeader[GVStream];
nextItemIndex ← GVStream.GetIndex[] +
(itemHeader.length+ bpw-1)/bpw*bpw --word boundary--;
continue ← proc[itemHeader, GVStream, procData];
IF (itemHeader.type = LastItem) OR (NOT continue) THEN EXIT;
GVStream.SetIndex[nextItemIndex];
ENDLOOP;
};
bpw: INT = Basics.bytesPerWord;
ReceiveRName: PUBLIC PROC[GVStream: STREAM] RETURNS [ROPE] = {
ENABLE GVProtocol.Failed =>
IF why = protocolError THEN
ERROR SyntaxError[Rope.Concat["failed to read RName: ", text]];
RETURN[GVProtocol.ReceiveRName[GVStream]]; };
ReceiveCount: PUBLIC PROC[GVStream: STREAM] RETURNS [CARDINAL] = {
ENABLE GVProtocol.Failed =>
IF why = protocolError THEN
ERROR SyntaxError[Rope.Concat["failed to read count: ", text]];
RETURN[LOOPHOLE[GVProtocol.ReceiveCount[GVStream]]]; };
SyntaxError: PUBLIC ERROR [reason: ROPE] ~ CODE;
PrintGVItem: PUBLIC ArpaSMTPSyntax.GVItemProc = {
out: STREAM ~ NARROW[procData];
BEGIN
ENABLE {
IO.EndOfStream => {out.PutRope["<<<unexpected EOS>>>\n"]; GOTO Return};
SyntaxError => {
out.PutRope["<<<syntax error: "]; out.PutRope[reason]; out.PutRope[">>>\n"];
GOTO Return; }; };
PutHeader: PROC [type: ROPE, raw: BOOL] = TRUSTED {
out.PutF["----- %g (%bB), %g bytes", [rope[type]], [integer[LOOPHOLE[itemHeader.type, CARDINAL]]], [integer[itemHeader.length]] ];
out.PutRope[IF raw THEN " (raw format) -----\n" ELSE " -----\n"];
IF itemHeader.length <= 0 THEN ERROR SyntaxError["length <= 0"]; };
PutRaw: PROC [] = { -- somewhat inefficient, but infrequently used
currentIndex: INT = itemStream.GetIndex[];
nBytesLeft: INT ← itemHeader.length;
itemRestrictedStream: STREAM = ArpaSMTPSupport.CreateSubrangeStream[itemStream, currentIndex, currentIndex + nBytesLeft];
buffer: REF TEXT = RefText.ObtainScratch[100];
WHILE nBytesLeft > 0 DO
Beware of bounds fault - count is a NAT
nBytesRead: INT ← itemRestrictedStream.GetBlock[
buffer, 0, MIN[nBytesLeft, buffer.maxLength]];
IF nBytesRead = 0 THEN ERROR IO.EndOfStream[itemRestrictedStream];
out.PutBlock[buffer, 0, nBytesRead];
nBytesLeft ← nBytesLeft - nBytesRead;
ENDLOOP;
out.PutChar['\n];
RefText.ReleaseScratch[buffer]; };
SELECT itemHeader.type FROM
PostMark => {
PutHeader["PostMark", FALSE];
out.PutRope[GVBasics.RopeFromTimestamp[GVProtocol.ReceiveTimestamp[itemStream]]];
out.PutChar['\n];};
Sender => {
PutHeader["Sender", FALSE];
out.PutRope[ReceiveRName[itemStream]];
out.PutChar['\n];};
ReturnTo => {
PutHeader["ReturnTo", FALSE];
out.PutRope[ReceiveRName[itemStream]];
out.PutChar['\n];};
Recipients => {
numRecips: INT = ReceiveCount[itemStream];
PutHeader["Recipients", FALSE];
THROUGH [1..numRecips] DO
out.PutRope[ReceiveRName[itemStream]]; out.PutChar['\n] ENDLOOP;};
Text => {PutHeader["Text", FALSE]; PutRaw[]};
Capability => {PutHeader["Capability", TRUE]; PutRaw[]};
Audio => {PutHeader["Audio", TRUE]; out.PutRope["<probably not worth printing>\n"]};
LastItem => {PutHeader["LastItem", FALSE]; out.PutChar['\n]};
ENDCASE => {PutHeader["", TRUE]; PutRaw[]};
EXITS Return => NULL;
END; };
BlessReturnPath: PUBLIC PROC [raw: ROPE] RETURNS [arpa: ROPE] =
BEGIN
1) Bitch if name of first host on return path isn't recognized by name servers
2) Make sure it ends in .ARPA (or such) so GV will send rejections back via us
length: INT = Rope.Length[raw];
host: ROPE;
IF Rope.Fetch[raw, 0] = '@ THEN { -- @Foo:X@Y case
FOR i: INT IN [1..length) DO
char: CHAR = Rope.Fetch[raw, i];
SELECT char FROM
',, ': => { host ← Rope.Substr[raw, 1, i-1]; EXIT; };
ENDCASE => NULL;
REPEAT FINISHED =>
ArpaSMTPSupport.Log[important, "Invalid syntax in return path: ", raw];
ENDLOOP; }
ELSE { --Foo@Bar
FOR i: INT DECREASING IN [0..length) DO
c: CHAR = Rope.Fetch[raw, i];
IF c = '@ THEN { host ← Rope.Substr[raw, i + 1, (length-i-1)]; EXIT; };
ENDLOOP; };
SELECT TRUE FROM
(host = NIL) => NULL;
~CheckHostName[host] =>
ArpaSMTPSupport.Log[important, "Invalid character in first return return host: ", raw];
ArpaName.AliasToName[host].status = bogus => {
ArpaSMTPSupport.Log[important, "BOGUS host name in return path: ", host]; };
ENDCASE => NULL;
SELECT TRUE FROM
Rope.Find[raw, ","] # -1 => {
@A,@B:User@Host => comma would be bogus in rejection msgs
arpa ← Rope.Cat["\"", raw, "\"@", ArpaConfig.ourLocalName];
};
ValidDomain[raw] =>
User@Host.ARPA or @Mumble:User@Host.ARPA
arpa ← raw;
raw.Fetch[0] = '@ => {
Can't fixup tail of name: it might be in a different name space
arpa ← Rope.Cat["\"", raw, "\"@", ArpaConfig.ourLocalName];
};
ENDCASE =>
User@Host (no .ARPA)
Somebody fed us an alias rather than the truth. Normalize it.
BEGIN
length: INT ← Rope.Length[raw];
user, host, newHost: ROPE;
FOR i: INT DECREASING IN [0..length) DO
c: CHAR = raw.Fetch[i];
IF c = '@ THEN {
user ← Rope.Substr[raw, 0, i];
host ← Rope.Substr[raw, i + 1, (length-i-1)];
IF ~Rope.IsEmpty[host] THEN
IF host.Fetch[0] = '[ THEN host ← Rope.Cat[host, ".ARPA"]; -- [36,1,2,6]
newHost ← NormalizeName[host];
IF Rope.IsEmpty[newHost] THEN {
Yetch. Somebody fed us a bogus name.
Rejection msgs from GV will probably not work.
ArpaSMTPSupport.Log[important, "Bogus return path: ", raw];
arpa ← Rope.Cat["\"", raw, "\"@", ArpaConfig.ourLocalName];
}
ELSE
arpa ← Rope.Cat[user, "@", newHost];
EXIT; };
REPEAT FINISHED => arpa ← raw;
ENDLOOP;
END;
IF arpa = NIL THEN arpa ← raw;
IF arpa # raw THEN ArpaSMTPSupport.Log[important, "Return path fixup: ", raw, " => ", arpa];
END;
ValidDomain: PROC [raw: ROPE] RETURNS [BOOLEAN] = {
FOR list: LIST OF Rope.ROPE ← ArpaConfig.validDomains, list.rest UNTIL list = NIL DO
domain: Rope.ROPE ← list.first;
IF DotTailed[raw, domain] THEN RETURN[TRUE];
ENDLOOP;
RETURN[FALSE];};
UnBlessReturnPath: PUBLIC PROC [raw: ROPE] RETURNS [arpa: ROPE] =
BEGIN
Strip "..."@Xerox.ARPA to be kind to other mailers
(the ones that aren't bright enough to process quotes)
IF raw = NIL THEN RETURN[NIL];
IF Rope.Fetch[raw, 0] #'" THEN RETURN[raw];
IF ~Tailed[raw, ArpaConfig.ourLocalName] THEN RETURN[raw];
RETURN[Rope.Substr[raw, 1, Rope.Length[raw]-3-Rope.Length[ArpaConfig.ourLocalName]]];
END;
ReversePath: PUBLIC PROC [gv: ROPE] RETURNS [arpa: ROPE] =
BEGIN
length: INT ← gv.Length[];
FOR i: INT DECREASING IN [0..length) DO
c: CHAR = gv.Fetch[i];
IF c = '\" THEN { -- "Foo" or "Foo".OSBUNorth
IF gv.Fetch[0] # '\" THEN EXIT;
IF i = length-1 THEN gv ← Rope.Substr[gv, 1, length-2]
ELSE gv ← Rope.Cat[Rope.Substr[gv, 1, i-1], Rope.Substr[gv, i+1, length-i-1]];
EXIT; };
IF c = '@ THEN {
glue: ROPE = IF gv.Fetch[0] = '@ THEN "," ELSE ":";
arpa ← Rope.Cat["@", ArpaConfig.ourLocalName, glue, gv];
RETURN; };
ENDLOOP;
gv ← FixupSpaces[gv];
gv ← MaybeAddQuotes[gv];
arpa ← Rope.Cat[gv, "@", ArpaConfig.ourLocalName]; -- Normal GV case
END;
HostAndUser: PUBLIC PROC [raw: ROPE] RETURNS [host, user: ROPE] = {
FOR list: LIST OF ROPE ← arpaSpecialRegistries, list.rest UNTIL list = NIL DO
raw ← FixupTail[raw, Rope.Cat[".", list.first]];
raw ← FixupTail[raw, Rope.Cat[".", list.first, ".ARPA"]];
ENDLOOP;
raw ← FixupTail[raw, ".ARPA.ARPA"];
FOR list: LIST OF Rope.ROPE ← ArpaConfig.validDomains, list.rest UNTIL list = NIL DO
domain: Rope.ROPE ← list.first;
rope: Rope.ROPE ← Rope.Cat[".", domain, ".ARPA"];
raw ← TruncateTail[raw, rope];
ENDLOOP;
IF Rope.Find[raw, "].ARPA", 0, FALSE] # -1 THEN raw ← TruncateTail[raw, ".ARPA"];
IF raw.Fetch[0] # '@ THEN {
Hackery to translate user@host.CSNet into user%host@relay.cs.net
Beware: There is similar code in MTTReeOpsImpl
raw ← Redirect[raw, ".BITNET", ArpaConfig.bitnetGateway];
raw ← Redirect[raw, ".CSNet", ArpaConfig.csnetGateway];
raw ← Redirect[raw, ".UUCP", ArpaConfig.uucpGateway] };
IF raw.Fetch[0] # '@ THEN {
Without the next lines, mail to ourselves will go around again!!! Good for testing, but..
raw ← StripTail[raw, Rope.Cat["@", ArpaConfig.ourLocalName]];
raw ← StripTail[raw, Rope.Cat["@", ConvertExtras.RopeFromArpaAddress[ArpaConfig.ourLocalAddress]]];
raw ← StripTail[raw, Rope.Cat["@", ConvertExtras.RopeFromArpaAddress[ArpaConfig.ourLocalAddress], ".ARPA"]];
FOR list: LIST OF ROPE ← possibleOldNames, list.rest UNTIL list = NIL DO
raw ← StripTail[raw, Rope.Cat["@", list.first]];
ENDLOOP;
FOR list: LIST OF ROPE ← possibleCurrentNames, list.rest UNTIL list = NIL DO
raw ← StripTail[raw, Rope.Cat["@", list.first]];
ENDLOOP;
}
ELSE {
Without the next line, mail to ourselves will go around again!!! Good for testing, but..
raw ← StripHead[raw, Rope.Cat["@", ArpaConfig.ourLocalName]];
raw ← StripHead[raw, Rope.Cat["@", ConvertExtras.RopeFromArpaAddress[ArpaConfig.ourLocalAddress]]];
raw ← StripHead[raw, Rope.Cat["@", ConvertExtras.RopeFromArpaAddress[ArpaConfig.ourLocalAddress], ".ARPA"]];
FOR list: LIST OF ROPE ← possibleOldNames, list.rest UNTIL list = NIL DO
raw ← StripHead[raw, Rope.Cat["@", list.first]];
ENDLOOP;
FOR list: LIST OF ROPE ← possibleCurrentNames, list.rest UNTIL list = NIL DO
raw ← StripHead[raw, Rope.Cat["@", list.first]];
ENDLOOP;
};
raw ← StripQuotes[raw];
[host: host, user: user] ← FindHostName[raw];
IF host = NIL THEN {
user ← StripTail[user, ".ARPA"]; -- Hack for testing by sending to Foo.PA.Arpa
user ← ForceRegistry[user];
user ← FixupUnderbars[user]; }; };
Redirect: PROC [old, domain, relay: ROPE] RETURNS [new: ROPE] =
BEGIN
tail: ROPE ← Rope.Cat[domain, ".ARPA"];
IF Tailed[old, tail] THEN old ← StripTail[old, ".ARPA"];
IF Tailed[old, domain] THEN {
length: INT;
old ← StripTail[old, domain];
length ← Rope.Length[old];
FOR i: INT DECREASING IN [0..length) DO
IF Rope.Fetch[old, i] = '@ THEN {
name: ROPE ← Rope.Substr[old, 0, i];
name ← StripQuotes[name]; -- "Joe User"@Host.xx
old ← Rope.Cat[name, "%", Rope.Substr[old, i + 1, (length-i-1)], domain];
old ← MaybeAddQuotes[old]; -- "Joe User%Host.xx"
EXIT; };
ENDLOOP;
old ← Rope.Cat[old, "@", relay]; };
RETURN[old];
END;
StripQuotes: PROC [old: ROPE] RETURNS [new: ROPE] = {
length: INT ← old.Length[];
new ← old;
IF length < 2 THEN RETURN;
IF old.Fetch[0] # '\" THEN RETURN;
SELECT TRUE FROM
old.Fetch[length-1] = '\" => new ← old.Substr[1, length-1-1];
Tailed[old, "\".ARPA"] => new ← old.Substr[1, length-1-6];
ENDCASE => RETURN;
length ← new.Length[];
FOR i: INT IN [0..length) DO
IF new.Fetch[i] = '\\ THEN EXIT;
REPEAT FINISHED => RETURN; -- No \ inside the string
ENDLOOP;
BEGIN
quoteSeen: BOOLEANFALSE;
text: REF TEXT ← RefText.ObtainScratch[length];
FOR i: INT IN [0..length) DO
c: CHAR = new.Fetch[i];
IF c = '\\ AND ~quoteSeen THEN { quoteSeen ← TRUE; LOOP; };
text ← RefText.AppendChar[text, c];
quoteSeen ← FALSE;
ENDLOOP;
new ← Rope.FromRefText[text];
RefText.ReleaseScratch[text];
END; };
MaybeAddQuotes: PROC [old: ROPE] RETURNS [new: ROPE] =
BEGIN
See pg 10 of RFC 822. An Atom is anything except specials, SPACE, and CTLs.
This won't do the right things with Foo..bar
length: INT ← old.Length[];
new ← old;
IF Rope.IsEmpty[new] THEN RETURN;
IF Rope.Fetch[new, 0] = '" THEN RETURN; -- Assume already quoted correctly
FOR i: INT IN [0..length) DO
SELECT Rope.Fetch[new, i] FROM
> 177C => EXIT; -- Funny characters. What should happen to these??
'(, '), '<, '>, '@, '<, ';, ':, '\\, '", '[, '] => EXIT; -- Specials EXCEPT PERIOD!
' => EXIT; -- Space
< 040C => EXIT; -- CTL
ENDCASE => NULL; -- Includes underbar
REPEAT FINISHED => RETURN; -- Nothing fancy inside the string
ENDLOOP;
new ← Rope.Cat["\"", old, "\""];
END;
FixupTail: PROC [old, tail: ROPE] RETURNS [new: ROPE] = {
new ← old;
IF Tailed[old, tail] THEN {
new ← StripTail[old, tail];
new ← Rope.Concat[new, ".ARPA"]; }; };
TruncateTail: PROC [old, tail: ROPE] RETURNS [new: ROPE] = {
new ← old;
IF Tailed[old, tail] THEN new ← StripTail[old, ".ARPA"]; };
Tailed: PROC [body, tail: ROPE] RETURNS [match: BOOL] = {
bodyLength: INT = body.Length[];
tailLength: INT = tail.Length[];
back: ROPE;
IF bodyLength <= tailLength THEN RETURN[FALSE];
back ← Rope.Substr[body, bodyLength-tailLength, tailLength];
IF Rope.Equal[back, tail, FALSE] THEN RETURN[TRUE];
RETURN[FALSE]; };
DotTailed: PROC [body, tail: ROPE] RETURNS [match: BOOL] = {
IF ~Tailed[body, tail] THEN RETURN[FALSE];
IF Rope.Fetch[body, Rope.Length[body]-Rope.Length[tail]-1] # '. THEN RETURN[FALSE];
RETURN[TRUE]; };
StripTail: PROC [body, tail: ROPE] RETURNS [new: ROPE] = {
bodyLength: INT = body.Length[];
tailLength: INT = tail.Length[];
IF ~Tailed[body, tail] THEN RETURN[body];
RETURN[Rope.Substr[body, 0, bodyLength - tailLength]]};
Headed: PROC [body, head: ROPE] RETURNS [match: BOOL] = {
bodyLength: INT = body.Length[];
headLength: INT = head.Length[];
char: CHAR;
IF bodyLength <= headLength+1 THEN RETURN[FALSE];
char ← Rope.Fetch[body, headLength];
IF char = ', OR char = ': THEN {
front: ROPE ← Rope.Substr[body, 0, headLength];
IF Rope.Equal[front, head, FALSE] THEN RETURN[TRUE]; };
RETURN[FALSE]; };
StripHead: PROC [body, head: ROPE] RETURNS [new: ROPE] = {
bodyLength: INT = body.Length[];
headLength: INT = head.Length[];
IF ~Headed[body, head] THEN RETURN[body];
NB: Remove following , or : too
RETURN[Rope.Substr[body, headLength+1, bodyLength - headLength -1]]};
FindHostName: PROC [raw: ROPE] RETURNS [host, user: ROPE] = {
length: INT ← raw.Length[];
newHost: ROPE;
user ← raw;
IF Rope.Fetch[raw, 0] = '@ THEN { -- @Foo:X@Y case
FOR i: INT IN [1..length) DO
char: CHAR = Rope.Fetch[raw, i];
SELECT char FROM
',, ': => {
host ← Rope.Substr[raw, 1, i-1];
IF ~CheckHostName[host] THEN RETURN["Invalid character in host name", raw];
newHost ← NormalizeName[host];
IF ~Rope.IsEmpty[newHost] THEN host ← newHost;
user ← Rope.Substr[raw, i + 1, (length-i-1)];
No quote checking on this path
user ← Rope.Cat["@", host, Rope.FromChar[char], user];
RETURN; };
ENDCASE => NULL;
REPEAT FINISHED => RETURN["Invalid Syntax", raw];
ENDLOOP; };
FOR i: INT DECREASING IN [0..length) DO
c: CHAR = raw.Fetch[i];
IF c = '\" THEN EXIT;
IF c = '@ THEN { --Foo@Bar
user ← Rope.Substr[raw, 0, i];
host ← Rope.Substr[raw, i + 1, (length-i-1)];
IF ~CheckHostName[host] THEN RETURN["Invalid character in host name", raw];
newHost ← NormalizeName[host];
IF ~Rope.IsEmpty[newHost] THEN host ← newHost;
user ← MaybeAddQuotes[user]; -- "Foo Foo"@Bar
user ← Rope.Cat[user, "@", host];
RETURN; };
ENDLOOP;
FOR i: INT DECREASING IN [0..length) DO
c: CHAR = raw.Fetch[i];
IF c = '\" THEN EXIT;
IF c = '% THEN { -- Hackery: foo%bar
user ← Rope.Substr[raw, 0, i];
host ← Rope.Substr[raw, i + 1, (length-i-1)];
IF ~CheckHostName[host] THEN RETURN["Invalid character in host name", raw];
newHost ← NormalizeName[host];
IF ~Rope.IsEmpty[newHost] THEN host ← newHost;
user ← MaybeAddQuotes[user];
user ← Rope.Cat[user, "@", host];
RETURN; };
ENDLOOP;
host ← NIL; }; -- No @, Must be GV
CheckHostName: PROC [host: ROPE] RETURNS [ok: BOOLEAN] =
BEGIN
length: INT ← Rope.Length[host];
FOR i: INT IN [1..length) DO
char: CHAR = Rope.Fetch[host, i];
SELECT char FROM
'(, '), '<, '>, '@, '<, ';, ':, '\\, '" => RETURN[FALSE]; -- Specials EXCEPT PERIOD and []!
' => RETURN[FALSE]; -- Space
< 040C => RETURN[FALSE]; -- CTL
ENDCASE => NULL;
ENDLOOP;
RETURN[TRUE];
END;
NormalizeName: PUBLIC PROC [raw: ROPE] RETURNS [host: ROPE] =
BEGIN
IF Rope.IsEmpty[raw] THEN RETURN[NIL];
raw ← StripTail[raw, ".ARPA"]; -- fix foo.EDU.ARPA case
IF raw.Fetch[0] = '[ THEN
IF ~DotTailed[raw, "ARPA"] THEN RETURN[Rope.Cat[raw, ".ARPA"]];
IF Rope.Find[raw, ".",, FALSE] = -1 THEN raw ← Rope.Cat[raw, ".ARPA"];
IF SpecialDomain[raw] THEN RETURN[raw];
host ← ArpaName.AliasToName[raw, ArpaConfig.resolv^].name;
RETURN[host];
END;
SpecialDomain: PROC [raw: ROPE] RETURNS [BOOLEAN] = {
FOR list: LIST OF Rope.ROPE ← ArpaConfig.specialDomains, list.rest UNTIL list = NIL DO
domain: Rope.ROPE ← list.first;
IF DotTailed[raw, domain] THEN RETURN[TRUE];
ENDLOOP;
RETURN[FALSE];};
ForceRegistry: PROC [raw: ROPE] RETURNS [user: ROPE] =
BEGIN
length: INT ← raw.Length[];
user ← raw;
FOR i: INT DECREASING IN [0..length) DO
IF raw.Fetch[i] = '. THEN RETURN;
ENDLOOP;
user ← Rope.Concat[raw, ArpaSMTPControl.defaultRegistry];
END;
FixupSpaces: PROC [raw: ROPE] RETURNS [user: ROPE] =
BEGIN
length: INT ← raw.Length[];
user ← raw;
FOR i: INT IN [0..length) DO
IF raw.Fetch[i] = ' THEN EXIT;
REPEAT FINISHED => RETURN;
ENDLOOP;
user ← Rope.Translate[base: user, translator: SpaceToUnderbar];
END;
FixupUnderbars: PROC [raw: ROPE] RETURNS [user: ROPE] =
BEGIN
length: INT ← raw.Length[];
name, registry: ROPE;
user ← raw;
FOR i: INT IN [0..length) DO
IF raw.Fetch[i] = '← THEN EXIT;
REPEAT FINISHED => RETURN;
ENDLOOP;
FOR i: INT DECREASING IN [0..length) DO
IF raw.Fetch[i] = '. THEN {
name ← Rope.Substr[raw, 0, i];
registry ← Rope.Substr[raw, i, (length-i)]; -- Registry includes the dot
EXIT; };
REPEAT FINISHED => name ← user; -- No registry (?)
ENDLOOP;
name ← Rope.Translate[base: name, translator: UnderbarToSpace];
user ← Rope.Cat["\"", name, "\"", registry];
END;
UnderbarToSpace: PROC [old: CHAR] RETURNS [new: CHAR] =
BEGIN
IF old = '← THEN RETURN[' ] ELSE RETURN[old];
END;
SpaceToUnderbar: PROC [old: CHAR] RETURNS [new: CHAR] =
BEGIN
IF old = ' THEN RETURN['←] ELSE RETURN[old];
END;
END.