TITLE MAISER TOPS-20 SMTP mail server
       SUBTTL Written by Mark Crispin - November 1982

; Copyright 1982-2007 Mark Crispin, Bainbridge Island, WA
; All rights reserved

; Version components

MLSWHO==0                       ; who last edited MAISER (0=developers)
MLSVER==7                       ; MAISER's release version (matches monitor's)
MLSMIN==1                       ; MAISER's minor version
MLSEDT==^D202                   ; MAISER's edit version

       SEARCH MACSYM,MONSYM    ; system definitions
       SALL                    ; suppress macro expansions
       .DIRECTIVE FLBLST       ; sane listings for ASCIZ, etc.
       .TEXT "/NOINITIAL"      ; suppress loading of JOBDAT
       .TEXT "MAISER/SAVE"     ; save as MAISER.EXE
       .TEXT "/SYMSEG:PSECT:CODE" ; put symbol table and patch area in CODE
       .REQUIRE HSTNAM         ; host name routines
       .REQUIRE WAKEUP         ; MMailr wakeup routine
       .REQUIRE SYS:MACREL     ; MACSYM support routines
IFNDEF OT%822,OT%822==:1

;  MAISER is the server to receive electronic mail from other systems via
; a network.  It implements the server half of SMTP (Simple Mail Transfer
; Protocol), the DoD standard electronic mail interchange protocol defined
; in RFC 2821, and documented online on the Internet as:
;       ftp://ftp.ietf.org/rfc/rfc2821.txt
;
;  While nominally MAISER will be used layered on top of the DoD transport
; protocols (TCP/IP) in the Internet environment, it has been designed so
; that this is not necessary.  All I/O is done via primary I/O, and the
; Internet system call dependencies have been kept to a minimum so that the
; server can essentially support any network.
;
;  MAISER runs on TOPS-20 release 5.3 and later monitors.  MAISER will not
; run on Tenex; the "Twenex" operating system is a figment of the imagination
; of certain individuals.  There ain't no such thing as a free lunch.

; Routines invoked externally

       EXTERN $GTPRO,$GTNAM,$GTLCL,$GTHNS,$GTHSN,$GTHRL,$GTHWL,$RMREL,$GTHST
       EXTERN $WAKE
       EXTERN $CHSSN,$CHSNS,$GTCAN
      SUBTTL Assembly options

IFNDEF FT2821,<FT2821==1>       ; RFC 2821 (as opposed to RFC 822) compliance
IFNDEF TIMOCT,<TIMOCT==^D20>    ; number of 15-second ticks of inactivity
                               ;  allowed before autologout
IFNDEF MAXSIZE,<MAXSIZE=^D65536> ; maximum size message permitted
IFNDEF FTSTALL,<FTSTALL==1>     ; stall on certain errors to delay hackers

IFN FT2821,<FTDATABUG==0>       ; forbidden to set this if RFC compliant
IFNDEF FTUNIXBUG,<FTUNIXBUG==0> ; non-zero to compensate for stupid UNIX SMTP
                               ;  servers that think that Internet newline is
                               ;  bare LF.  Not strictly RFC compliant, but
                               ;  not forbidden either
IFNDEF FTDATABUG,<FTDATABUG==0> ; non-zero to accept <LF>.<LF> as equivalent
                               ;  to <CRLF>.<CRLF>   Forbidden by RFC 2821
IF2,<IFN FTDATABUG,<            ;; don't remove this
PRINTX %You have configured this server to be in VIOLATION of the SMTP
PRINTX % standard.  Refer to RFC 2821, section 4.1.1.4 (on page 33):
PRINTX %  The custom of accepting lines ending only in <LF>, as a concession to
PRINTX %  non-conforming behavior on the part of some UNIX systems, has proven
PRINTX %  to cause more interoperability problems than it solves, and SMTP
PRINTX %  server systems MUST NOT do this, even in the name of improved
PRINTX %  robustness.  In particular, the sequence "<LF>.<LF>" (bare line
PRINTX %  feeds, without carriage returns) MUST NOT be treated as equivalent to
PRINTX %  <CRLF>.<CRLF> as the end of mail data indication.
>>

; From here on down probably do not need site-specific customization

IFNDEF DATORG,<DATORG==3000>    ; data on page 3
IFNDEF PAGORG,<PAGORG==100000>  ; paged data on page 100
IFNDEF CODORG,<CODORG==400000>  ; code on page 400

;  These fields have required minimum sizes established by RFC 2821.  Someday
; these ought to be made to be dynamically assigned out of free storage.

IFNDEF TXTLEN,<TXTLEN==2*^D512> ; length of command line (512 required minimum)
IFNDEF ADLLEN,<ADLLEN==2*^D256> ; length of an a-d-l (256 required minimum)
IFNDEF USRNML,<USRNML==2*^D64>  ; length of a user name (64 required minimum)
IFNDEF HSTNML,<HSTNML==2*^D255> ; length of a host name (255 required minimum)
                               ;  (formerly 64 in RFC 821)
                               ; no limit to text line (1000 required minumum)

IFNDEF GFKFKS,<GFKFKS==1>       ; number of forks gotten by GFRKS%
IFNDEF PDLLEN,<PDLLEN==^D2000>  ; stack length
      SUBTTL Definitions

; AC definitions

FL==:0                          ; flags
A=:1                            ; JSYS, temporary ACs
B=:2
C=:3
D=:4
E=:5                            ; non-JSYS temporary ACs
F=:6
G=:7
H=:10
P=:17                           ; stack pointer

; Flags

       MSKSTR F%HLO,FL,1B0     ; HELO command seen
       MSKSTR F%FRM,FL,1B1     ; have a FROM specification
       MSKSTR F%TO,FL,1B2      ; have a TO specification
       MSKSTR F%EOL,FL,1B3     ; EOL seen
       MSKSTR F%ELP,FL,1B4     ; buffer began with EOL
       MSKSTR F%EXP,FL,1B5     ; EXPN vs. VRFY command
       MSKSTR F%DOP,FL,3B7     ; delivery option code (see DOPTAB)
       MSKSTR F%NOK,FL,1B8     ; PARMBX allows null path (for MAIL FROM:)
       MSKSTR F%MOK,FL,1B9     ; PARMBX allows null domain (for RCPT TO:)
       MSKSTR F%VLH,FL,1B10    ; given host name validated
       MSKSTR F%REE,FL,1B11    ; reenter
       MSKSTR F%NVT,FL,1B12    ; on a network terminal, must log out when done
       MSKSTR F%RFS,FL,1B13    ; found a user who's refusing sends
       MSKSTR F%PRO,FL,3B15    ; transport protocol:
        P%UNK==0               ; unknown
        P%TCP==1               ; TCP
        P%CHA==2               ; Chaosnet
        P%MAX==3               ; Max number of possible transport protocols
       MSKSTR F%QOT,FL,1B16    ; doing quoting
       MSKSTR F%JFN,FL,1B17    ; primary I/O is a JFN that must be closed
       MSKSTR F%NAH,FL,1B18    ; not validated name
       MSKSTR F%EHL,FL,1B19    ; EHLO command seen

; Here's a macro that really should be in MACSYM!

DEFINE ANNJE. <..TAGF (ERJMP,)>

; Fatal assembly error macro

DEFINE .FATAL (MESSAGE) <
PASS2
PRINTX ?'MESSAGE
END
>;DEFINE .FATAL

CHLPR==:"("                     ; work around various macro lossages
CHRPR==:")"
CHLAB==:"<"
CHRAB==:">"
CHQOT==:""""

       SUBTTL GTDOM% definitions

IFNDEF GTDOM%,<
       OPDEF GTDOM% [JSYS 765]

GD%LDO==:1B0                    ; local data only (no resolve)
GD%MBA==:1B1                    ; must be authoritative (don't use cache)
GD%RBK==:1B6                    ; resolve in background
GD%EMO==:1B12                   ; exact match only
GD%RAI==:1B13                   ; uppercase output name
GD%QCL==:1B14                   ; query class specified
GD%STA==:1B16                   ; want status code in AC1 for marginal success
 .GTDX0==:0                    ; total success
 .GTDXN==:1                    ; data not found in namespace (authoritative)
 .GTDXT==:2                    ; timeout, any flavor
 .GTDXF==:3                    ; namespace is corrupt

GTDWT==:12                      ; resolver wait function
GTDPN==:14                      ; get primary name and IP address
GTDMX==:15                      ; get MX (mail relay) data
 .GTDLN==:0                    ; length of argblk (inclusive)
 .GTDTC==:1                    ; QTYPE (ignored for .GTDMX),,QCLASS
 .GTDBC==:2                    ; length of output string buffer
 .GTDNM==:3                    ; canonicalized name on return
 .GTDRD==:4                    ; returned data begins here
 .GTDML==:5                    ; minimum length of argblock (words)
GTDAA==:16                      ; authenticate address
GTDRR==:17                      ; get arbitrary RR (MIT formatted RRs)
>;IFNDEF GTDOM%

       SUBTTL Impure storage

       LOC 20                  ; enter low memory

FATACS: BLOCK 20                ; save of fatal ACs
JBUUO:  BLOCK 1                 ; LUUO saved here
JB41:   JSR UUOPC               ; instruction executed on LUUO
UUOACS: BLOCK 20                ; save of UUO ACs

       LOC 116
JBSYM:  BLOCK 1                 ; symbol table pointer
JBUSY:  BLOCK 1                 ; place holder

       RELOC                   ; enter low segment

; Anti-spam settings, set non-zero to enable

$ASRES: 0                       ; foreign address must resolve to a name
$ASRVH: 0                       ; HELO/EHLO validation
$ASRCP: 0                       ; disable RCPT address validation
$ASVFY: 0                       ; disable VRFY, implies $ASEXP
$ASEXP: 0                       ; disable EXPN
$ASGRP: 0                       ; greeting pause
$ASHLO: 0                       ; reject localhost, mail.local in HELO/EHLO
$ASCBI: 0                       ; clear input buffer at each comand

; UUO handler

UUOPC:  BLOCK 1                 ; PC of LUUO
       MOVEM 17,FATACS+17      ; save ACs in FATACS for debugging
       MOVEI 17,FATACS         ; save from 0 => FATACS
       BLT 17,FATACS+16        ; ...to 16 => FATACS+16
       MOVE 17,FATACS+17       ; restore AC17
       TMSG <421-Illegal instruction >
       MOVX A,.PRIOU           ; output the losing LUUO
       MOVE B,.JBUUO
       MOVX C,^D8              ; in octal
       NOUT%
        NOP
       TMSG < at >
       HRRZ F,UUOPC            ; output PC which lost
       CALL SYMOUT
       JRST IMPERR             ; indicate impossible error and die

; Data area

       .PSECT DATA,DATORG      ; enter data area

PDL:    BLOCK PDLLEN            ; stack
BUFFER: BLOCK <TXTLEN/5>+1      ; general purpose buffer
GTJBLK: BLOCK <.JIBAT-.JITNO+1> ; GETJI% stores data here
TMPBUF: BLOCK 30                ; temporary buffer
IN2ACS: BLOCK 3                 ; save area for ACs A-C, level 2
LEV1PC: BLOCK 1                 ; PSI level 1 PC
LEV2PC: BLOCK 1                 ; PSI level 2 PC
LEV3PC: BLOCK 1                 ; PSI level 3 PC
TIMOUT: BLOCK 1                 ; timeout count

INICBG==.                       ; first location cleared at once-only init
MYUSRN: BLOCK 1                 ; my user number
       ; Following two lines must be in this order
MYJOBN: BLOCK 1                 ; my job number
MYTTYN: BLOCK 1                 ; my TTY number
       ; end of critical order data
MBXFRK: BLOCK 1                 ; mailbox fork
MBXWIN: BLOCK 1                 ; current window pointer into mailbox

; Host name/address storage

LCLHNO: BLOCK 1                 ; local host address from STAT%
LCLHNC: BLOCK 1                 ; local host address (in canonical form)
LCLHST: BLOCK <HSTNML/5>+1      ; local host name
FRNHNO: BLOCK 1                 ; foreign host address from STAT%
FRNHST: BLOCK <HSTNML/5>+1      ; foreign host name from FRNHNO
FRNHNM: BLOCK <HSTNML/5>+1      ; foreign host name from HELO negotiation

RSTCBG==.                       ; first location cleared at RSET time
MLQJFN: BLOCK 1                 ; queued mail file JFN
MBXBEG==.                       ; first mailbox location
ATDOML: BLOCK <ADLLEN/5>+1      ; at domain list specification
MAILBX: BLOCK <USRNML/5>+1      ; mailbox specification
DOMAIN: BLOCK <HSTNML/5>+1      ; domain specification
MBXEND==.-1                     ; last path location
RSTCEN==.-1                     ; last location cleared at RSET time
INICEN==.-1                     ; last location cleared at once-only init

       .ENDPS

; Paged data area

       .PSECT DATPAG,PAGORG    ; data pages

MBXPAG: BLOCK 2000              ; for mailing list forwarding pointers
WINPAG: BLOCK 2000              ; for mailing list forwarding strings

       .ENDPS
      SUBTTL Start of program

       .PSECT CODE,CODORG      ; pure code

; Entry vector

EVEC:   JRST MAISER             ; START address
       JRST MAIREE             ; REENTER address
       <FLD MLSWHO,VI%WHO>!<FLD MLSVER,VI%MAJ>!<FLD MLSMIN,VI%MIN>!<FLD MLSEDT,VI%EDN>!VI%DEC
EVECL==.-EVEC

MAISER: TDZA FL,FL              ; clear flags
MAIREE:  MOVX FL,F%REE
       RESET%                  ; flush all I/O
       MOVE P,[IOWD PDLLEN,PDL] ; init stack context
       SETZM INICBG            ; clear once-only area
       MOVE A,[INICBG,,INICBG+1]
       BLT A,INICEN

;  It looks like a bad idea to run with capabilities, and it is.  However, a
; system which runs with account validation may cause problems when trying
; to write the queued mail file.  We also want to avoid possible problems
; with protections or quotas in the queued mail directory.

       MOVX A,.FHSLF           ; get my capabilities
       RPCAP%
       IOR C,B                 ; enable as many capabilities as we can
       EPCAP%
        ERJMP .+1              ; ignore possible ACJ ITRAP
       SETZM TIMOCT            ; reset timeout count
       CALL SETPSI             ; set up PSIs

;  See if top-level fork, and if so assume we're a network server on an NVT.
; Note that all I/O is done via primary I/O.  This allows several ways we can
; be set up, e.g.:
; . traditional CRJOB% style running as a job on an NVT
; . on a physical terminal, as in a "TTY network" environment.
; . with primary I/O remapped to the network JFN's.

       GJINF%                  ; get job info
       MOVEM A,MYUSRN          ; save my user number
       DMOVEM C,MYJOBN         ; save job number/TTY number for later use
       IFGE. D                 ; can be NVT server only if attached
         MOVX A,.FHSLF         ; see what my primary I/O looks like.  If
         GPJFN%                ;  AC2 isn't -1 (.CTTRM,,.CTTRM), then we
         ..TAGF (<AOJN B,>,)   ;  can assume setup process init'd TTY
         MOVX A,.FHTOP         ; top fork
         SETZ B,               ; no handles or status
         MOVE C,[-<<GFKFKS*3>+1>,,BUFFER] ; fork structure area
         GFRKS%                ; look at fork structure
          ERJMP .+1            ; ignore error (probably GFKSX1)
         HRRZ A,BUFFER+1       ; get the top fork's handle
         CAXE A,.FHSLF         ; same as me?
         IFSKP.
           MOVX A,.PRIIN       ; set terminal type to ideal
           MOVX B,.TTIDL
           STTYP%
           MOVE B,[TT%MFF!TT%TAB!TT%LCA!TT%WKF!TT%WKN!TT%WKP!TT%WKA!<FLD .TTASC,TT%DAM>!<FLD .TTLDX,TT%DUM>]
           SFMOD%              ; has formfeed, tab, lowercase, all wakeup,
           STPAR%              ;  no translate ASCII, line half-duplex
           DMOVE B,[BYTE (2)2,2,2,0,2,2,2,2,2,2,2,2,2,2,2,2,2,2
                    BYTE (2)2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2]
           SFCOC%              ; disable all echoing on controls
           MOVX A,TL%CRO!TL%COR!TL%SAB!.RHALF ; break and refuse links
           MOVX B,.RHALF
           TLINK%
            ERCAL FATAL
           MOVX A,.PRIIN       ; refuse system messages
           MOVX B,.MOSNT
           MOVX C,.MOSMN
           MTOPR%
            ERCAL FATAL
           MOVE A,[SIXBIT/MAISER/] ; set our name
           SETNM%
           TQO F%NVT           ; flag an NVT server
         ENDIF.
       ENDIF.

; Get host info

       CALL GETTCP             ; get TCP local/foreign host poop
       IFNSK.
         CALL GETCHA           ; failed, try Chaosnet
       ANNSK.
;; calls for other networks go here
         HRROI A,LCLHST        ; otherwise get local host name any way we can
         CALL $GTLCL
         IFNSK.
           TMSG <421-Unable to get local host name>
           JRST IMPERR
         ENDIF.
         HRROI A,LCLHST        ; remove relative relative domain from name
         CALL $RMREL
       ENDIF.

; See if SYSTEM:DISABLE-MAIL.FLAG exists, and if so hang up

       MOVX A,GJ%SHT!GJ%OLD    ; check if mail disabled now
       HRROI B,[ASCIZ/SYSTEM:DISABLE-MAIL.FLAG/]
       GTJFN%                  ; by seeing if this magic file exists
       IFNJE.
         RLJFN%                ; it does, flush the JFN we made
          NOP
         TMSG <421->
         HRROI A,LCLHST        ; output host name
         PSOUT%
         TMSG < ESMTP service is disabled, please try again later
421 >
         JRST QUIT1
       ENDIF.

; Here to output a banner announcing the service

       SKIPE A,$ASGRP          ; get the anti-spam greet pause
        DISMS%
       MOVX A,.PRIIN           ; don't let client jump the gun either
       CFIBF%
        ERJMP .+1
       TMSG <220 >             ; start banner
       HRROI A,LCLHST          ; output host name
       PSOUT%
       TMSG < ESMTP >          ; we offer ESMTP now
       MOVX A,.PRIOU           ; set up for primary output
       LOAD B,VI%MAJ,EVEC+2    ; get major version
       MOVX C,^D10             ; versions are decimal as of 7/2005
       NOUT%
        ERCAL FATAL
       LOAD B,VI%MIN,EVEC+2    ; get minor version
       IFN. B                  ; ignore if no minor version
         MOVEI A,"."           ; output delimiting dot
         PBOUT%
         MOVX A,.PRIOU         ; now output the minor version
         NOUT%
          ERCAL FATAL
       ENDIF.
       LOAD B,VI%EDN,EVEC+2    ; get edit version
       IFN. B                  ; ignore if no edit version
         MOVX A,.CHLPR         ; edit delimiter
         PBOUT%
         MOVX A,.PRIOU         ; now output the edit version
         NOUT%
          ERCAL FATAL
         MOVX A,.CHRPR         ; edit close delimiter
         PBOUT%
       ENDIF.
       LOAD B,VI%WHO,EVEC+2    ; get who last edited
       IFN. B                  ; ignore if last edited by developers
         MOVX A,.CHHYP         ; output delimiting hyphen
         PBOUT%
         MOVX A,.PRIOU         ; now output the who version
         NOUT%
          ERCAL FATAL
       ENDIF.
       TMSG < at >
       MOVX A,.PRIOU           ; output date/time
       SETO B,                 ; time now
       MOVX C,OT%DAY!OT%SPA!OT%TMZ!OT%SCL!OT%822 ; RFC 822 standard date/time
       ODTIM%
        ERCAL FATAL
;       JRST GETCMD
      SUBTTL Command loop

GETCMD: DO.
         MOVX A,.PRIIN
         SKIPE $ASCBI          ; want to stymie streaming spammers?
          CFIBF%               ; yes, do so
           ERJMP .+1
         CALL CRLF             ; terminate reply with CRLF
         MOVNI A,TIMOCT        ; reset timeout count
         MOVEM A,TIMOUT
         SETZM BUFFER          ; clear out old crud in BUFFER
         MOVE A,[BUFFER,,BUFFER+1]
         BLT A,BUFFER+<TXTLEN/5>
         MOVX A,.PRIIN         ; from primary input
         HRROI B,BUFFER        ; pointer to command buffer
         MOVX C,TXTLEN-1       ; up to this many characters
IFE FTUNIXBUG,<
         MOVX D,.CHCRT         ; terminate on carriage return
>;IFE FTUNIXBUG
IFN FTUNIXBUG,<
         MOVX D,.CHLFD         ; terminate on line feed
>;IFN FTUNIXBUG
         SIN%                  ; read a command
          ERJMP INPEOF         ; finish up on error
         IFE. C                ; if count unsatisfied, must have seen CR
           LDB A,B             ; get last byte
IFE FTUNIXBUG,<
           CAXN A,.CHCRT       ; was it a CR?
>;IFE FTUNIXBUG
IFN FTUNIXBUG,<
           CAXN A,.CHLFD       ; was it a line feed?
>;IFN FTUNIXBUG
           IFSKP.
             TMSG <500 Line too long>
             LOOP.
           ENDIF.
         ENDIF.
IFE FTUNIXBUG,<
         PBIN%                 ; get expected LF
          ERJMP INPEOF         ; finish up on error
         CAXN A,.CHLFD         ; was it a line feed?
         IFSKP.
           TMSG <500 Line does not end with CRLF>
           LOOP.
         ENDIF.
>;IFE FTUNIXBUG
IFN FTUNIXBUG,<
         SETO C,               ; point to character before the last
         ADJBP C,B
         MOVE B,C              ; remember that pointer
         LDB C,C               ; get character before the last
         CAXE C,.CHCRT         ; was it a CR?
          IBP B                ; no, line ended with bare LF then
>;IFN FTUNIXBUG
         SETZB A,TIMOUT        ; make command null-terminated
         DPB A,B
         LDB C,[POINT 7,BUFFER,34] ; make sure space or NUL
         CAXE C,.CHSPC
          JUMPN C,SYNERR
         MOVE A,BUFFER         ; get command from buffer
         ANDCM A,[BYTE (7) 040,040,040,040,177] ; upper caseify
         MOVSI B,-CMDTBL       ; length of command table
         DO.
           CAME A,CMDTAB(B)    ; command matches?
            AOBJN B,TOP.       ; try next command
         ENDDO.
         JRST @CMDDSP(B)       ; dispatch to command
       ENDDO.
      SUBTTL Command table and dispatch

DEFINE COMMANDS <
; "Minimum required for an SMTP implementation" commands
       CMD EHLO
       CMD HELO
       CMD MAIL
       CMD RCPT
       CMD DATA
       CMD RSET
       CMD NOOP
       CMD QUIT
; "Optional" commands
       CMD SEND
       CMD SOML
       CMD SAML
       CMD VRFY
       CMD EXPN
       CMD HELP
       CMD TURN
>;DEFINE COMMANDS

DEFINE CMD (CM) <ASCII/'CM'/>

CMDTAB: COMMANDS                ; command names
CMDTBL==.-CMDTAB

DEFINE CMD (CM) <.'CM>

CMDDSP: COMMANDS                ; command dispatch
       BADCMD                  ; here if command not found
      SUBTTL Command service routines

; HELO - HELLO: negotiate identities

EHLO:   TQOA F%EHL              ; flag extended
HELO:    TQZ F%EHL              ; not extended
       TQZ <F%HLO,F%VLH>       ; cancel valid HELO and host validated
       JUMPE C,MISARG          ; must have argument
       SETZM FRNHNM
       DMOVE A,[POINT 7,BUFFER+1 ; pointer to foreign host name
                POINT 7,FRNHNM] ; where we store it
       MOVX D,HSTNML           ; length of a host name
       CALL GETDOM             ; get domain name
        JRST SYNFLD
       JUMPN C,SYNFLD          ; error if not newline here
       LOAD A,F%PRO            ; get protocol used
       CALL @VALDSP(A)         ; validate HELO according to transport protocol
       IFQN. F%HLO             ; have a valid HELO?
         TQNN F%EHL            ; EHLO?
          SKIPA A,[-1,,[ASCIZ/250 /]] ; HELO
           HRROI A,[ASCIZ/250-/] ; EHLO
       ELSE.
         HRROI A,[ASCIZ/421 /] ; HELO failure reply
       ENDIF.
       PSOUT%
       HRROI A,LCLHST          ; output our name
       PSOUT%
       TQNN F%VLH              ; host name validated?
        SKIPA A,D              ; no, output auxillary message
         HRROI A,[ASCIZ/ - Hello/]
       PSOUT%
       SKIPN FRNHST            ; do we know who foreign host is?
       IFSKP.
         TMSG <, >             ; yes, prepare to output it
         HRROI A,FRNHST        ; output foreign host's registered name
         PSOUT%
       ENDIF.
       JE F%HLO,,QUIT2         ; die if failed HELO
       IFQN. F%EHL             ; doing EHLO?
         SKIPN $ASVFY          ; VRFY disabled?
          SKIPE $ASEXP         ; EXPN disabled?
         IFSKP.
           TMSG <
250-EXPN>
         ENDIF.
         TMSG <
250-SIZE >
         MOVX A,.PRIOU         ; output size limit
         MOVX B,MAXSIZE
         MOVX C,^D10
         NOUT%
          ERCAL FATAL
         TMSG <
250-SEND
250-SOML
250-SAML
250 HELP>
       ENDIF.
       JRST RSET2              ; enter RSET code

;; Dispatch table for validation per transport protocol.
;; Any mismatch here will cause an error at the definitions above on pass 2.
VALDSP: PHASE 0
P%UNK:! VALUNK                  ; unknown protocol
P%TCP:! VALTCP                  ; TCP/SMTP
P%CHA:! VALCHA                  ; Chaos/SMTP
REPEAT <P%MAX-.>,<VALERR>       ; who knows?
       DEPHASE

;; Unknown protocol, no name validation possible
VALUNK: HRROI D,[ASCIZ/ - Your name accepted but not validated/]
       TQO F%HLO               ; HELO is valid, name is not
       RET                     ; that's all we can do.

;; Unrecognized F%PRO value, lose
VALERR: TMSG <421-Bad F%PRO dispatch (VALERR)>
       JRST IMPERR             ; "impossible" error, punt

;; Transport protocol is TCP/IP

VALTCP: SKIPE FRNHST            ; got foreign host name yet?
       IFSKP.
         HRROI A,FRNHST        ; get foreign host name
         MOVE B,FRNHNO         ; from foreign address
         CALL $GTHNS
         IFNSK.
           TMSG <421-Unable to get foreign host name>
           JRST IMPERR
         ENDIF.
         HRROI A,FRNHST        ; remove relative domain from name
         CALL $RMREL
         CALL $GTHRL           ; see if name is a literal
         IFSKP.
           SKIPN $ASRES        ; it is, punt if we want PTR
         ANSKP.
           TMSG <421-We do not accept mail from unresolvable IP addresses
421 >
           JRST QUIT1
         ENDIF.
         MOVX A,.GTDAA         ; no, authenticate (in case PTR spoof)
         HRROI B,FRNHST        ; this name
         MOVE C,FRNHNO         ; must match this address
         CALL $GTHST
          TQO F%NAH            ; name did not authenticate
       ENDIF.
       HRROI A,FRNHNM          ; see if name is a literal
       CALL $GTHRL             ; parse it and return address in B
       IFSKP.
         CAME B,FRNHNO         ; read a literal, address matches?
         IFSKP.
           TQO <F%HLO,F%VLH>   ; yes, note host name validated
           RET
         ENDIF.
         MOVE C,B              ; in case needed to restore
         HRROI A,BUFFER        ; canonicalize address: get name for address
         CALL $GTHNS           ; (using IN-ADDR again)
         IFSKP.
           HRROI A,BUFFER
           CALL $RMREL
           HRROI A,BUFFER      ; see if that name matches
           HRROI B,FRNHST
           STCMP%
           IFE. A
             TQO <F%HLO,F%VLH> ; yes, note host name validated
             RET
           ENDIF.
           HRROI A,BUFFER      ; now get the address from the name
           CALL $GTHSN
            MOVE B,C           ; restore address after failure
         ELSE.
            MOVE B,C           ; restore address after failure
         ENDIF.

       ELSE.                   ; not a literal, must be real host name
         SKIPN $ASHLO          ; want basic HELO validation?
         IFSKP.
           MOVE B,[POINT 7,FRNHNM]
           DO.
             ILDB A,B          ; make sure DNS format name
             CAIE A,"."        ; found delimiter?
              JUMPN A,TOP.
           ENDDO.
           IFN. A
             HRROI A,FRNHNM    ; reject mail.local
             HRROI B,[ASCIZ/MAIL.LOCAL/]
             STCMP%
           ANDN. A
             HRROI A,FRNHNM    ; reject localhost
             HRROI B,[ASCIZ/LOCALHOST.LOCALDOMAIN/]
             STCMP%            ; got a match?
           ANDN. A
           ELSE.
             HRROI D,[ASCIZ/ - fix your SMTP sender/]
             RET
           ENDIF.
         ENDIF.
         HRROI A,FRNHNM        ; point to her claimed foreign host name
         HRROI B,FRNHST        ; compare with what we think it is
         STCMP%                ; got a match?
         IFE. A
           TQO <F%HLO,F%VLH>   ; yes, note host name validated
           RET
         ENDIF.

         HRROI A,FRNHNM        ; point to claimed name
         CALL $GTHSN           ; get its address
         IFSKP.
           CAME B,FRNHNO       ; matches what we think?
           IFSKP.
             TQO <F%HLO,F%VLH> ; looks good
             RET
           ENDIF.
           CAME B,LCLHNC       ; no, claims to be me?
            CAMN B,LCLHNO
           IFNSK.
             HRROI D,[ASCIZ/ - You can't impersonate me/]
             RET
           ENDIF.
           MOVE H,B            ; save address for later
           MOVX A,.GTDAA       ; authenticate address
           HRROI B,FRNHNM      ; from claimed name
           MOVE C,FRNHNO       ; and its address
           CALL $GTHST
           IFSKP.
             TQO <F%HLO,F%VLH> ; note validated if OK
             RET
           ENDIF.
           MOVE B,H            ; get back address
         ENDIF.
       ENDIF.
       CALLRET VALNET          ; join common network validation code

;; Transport protocol is Chaosnet.

VALCHA: SKIPE FRNHST            ; got foreign host name yet?
       IFSKP.
         HRROI A,FRNHST        ; get foreign host name
         MOVE B,FRNHNO         ; from foreign address
         CALL $CHSNS
         IFNSK.                ; should never happen, I guess
           TMSG <421-Unable to get foreign host name>
           JRST IMPERR
         ENDIF.
         HRROI A,FRNHST        ; remove relative domain from name
         CALL $RMREL
       ENDIF.
       HRROI A,FRNHNM          ; point to her claimed foreign host name
       HRROI B,FRNHST          ; compare with what we think it is
       STCMP%                  ; got a match?
       IFE. A
         TQO <F%HLO,F%VLH>     ; yes, note host name validated
         RET
       ENDIF.
       HRROI A,FRNHNM          ; point to claimed name
       CALL $CHSSN             ; get its address
        SETO B,                ; unknown name
       CAME B,FRNHNO           ; matches what we think?
       IFSKP.
         TQO <F%HLO,F%VLH>     ; looks good
         RET
       ENDIF.
       CAME B,LCLHNC           ; is it our local name?
       IFSKP.
         HRROI D,[ASCIZ/ - You can't impersonate me/]
         RET
       ENDIF.
;       CALLRET VALNET          ; join common network validation code

;; VALNET -- common code for validating network connections.
;; B/ address of claimed name

VALNET: IFQE. F%HLO             ; if we're still not certain...
         SKIPN $ASRVH          ; allow uncertain HELO?
          TQO F%HLO            ; yes, treat as valid anyway
         SKIPGE B
          SKIPA D,[-1,,[ASCIZ/ - Never heard of that name/]]
           HRROI D,[ASCIZ/ - You are a charlatan/]
       ENDIF.
       RET                     ; done in any case

; RSET - RESET state to initial

RSET:   JUMPN C,BADARG          ; can't have an argument
RSET1:  TMSG <250 OK>           ; acknowledge command
RSET2:  SKIPN A,MLQJFN          ; if a queue file open, flush its JFN
       IFSKP.
         TXO A,CZ%ABT          ; abort it
         CLOSF%
          ERCAL FATAL          ; why should this fail?
       ENDIF.
       SETZM RSTCBG            ; clear reset area
       MOVE A,[RSTCBG,,RSTCBG+1]
       BLT A,RSTCEN
       TQZ <F%FRM,F%TO>        ; no more FROM or TO specification known
       JRST GETCMD

; EXPN - EXPAND mailing list
; VRFY - VERIFY mailbox

EXPN:   TQOA F%EXP              ; flag expand
VRFY:    TQZ F%EXP              ; flag not expand
       JUMPE C,MISARG          ; must have an argument
       DMOVE A,[POINT 7,BUFFER+1 ; command argument
                POINT 7,MAILBX] ; where we load mailbox
       MOVX D,USRNML           ; maximum length of a name
       ILDB C,A                ; get first byte
       JUMPE C,MISARG          ; missing argument
       CAXE C,.CHQOT           ; quoted string?
       IFSKP.
         DO.
           ILDB C,A            ; get next byte to consider
           CAXN C,.CHQOT       ; end of quoted string?
           IFSKP.
             SOJL D,SYNFLD     ; no, make sure field isn't too large
             JUMPE C,SYNFLD    ; also make sure no premature end of line
             IDPB C,B          ; store byte in string
             LOOP.             ; get next byte
           ENDIF.
         ENDDO.
         ILDB C,A              ; get final byte
         JUMPN C,SYNFLD        ; make sure line ends here
       ELSE.
         DO.
           MOVEI E,(C)         ; get copy of character
           IDIVI E,^D32        ; E/ word to check, F/ bit to check
           MOVNS F
           MOVX G,1B0          ; make bit to check
           LSH G,(F)
           TDNE G,SPCMSK(E)    ; is it a special character?
            JRST SYNERR        ; it is, lose
           CAXE C,.CHRAB       ; disallow broket and at as specials
            CAIN C,"@"
             JRST SYNERR
           CAIN C,"\"          ; quote next byte literally?
            ILDB C,A           ; yes, get next byte
           IDPB C,B            ; store byte in string
           ILDB C,A            ; get next byte to consider
           SOJL D,SYNFLD       ; field too large
           JUMPN C,TOP.        ; if non-null, continue parse
         ENDDO.
       ENDIF.
       SKIPE $ASVFY            ; VRFY restricted?
        JRST NOVREX            ; yes, VRFY or EXPN not allowed
       SKIPN $ASEXP            ; EXPN restricted
       IFSKP.
         JN F%EXP,,NOVREX      ; no, disallow if EXPN
       ENDIF.
       IDPB C,B                ; tie off string
       HRROI A,MAILBX          ; point to mailbox
       CALL RUNMBX             ; validate address
       IFNSK.
         SKIPE MBXFRK          ; did mailbox fork run successfully?
         IFSKP.
           TMSG <451 Mailbox lookup process terminated abnormally>
           JRST GETCMD
         ENDIF.
         SKIPG MBXFRK          ; couldn't find mailbox fork?
          JRST NOTIMP          ; command not implemented
         TMSG <550 No such local mailbox as ">
         HRROI A,MAILBX        ; output the bad mailbox
         PSOUT%
         TMSG <", not verified>
         JRST GETCMD
       ENDIF.
       IFQE. F%EXP             ; EXPN or VRFY?
         TMSG (250 )           ; VRFY, just echo back the mailbox name given
         MOVX A,.CHLAB         ; MACRO still sucks after all these years
         PBOUT%
         HRROI A,MAILBX
         PSOUT%
         MOVX A,"@"
         PBOUT%
         HRROI A,LCLHST
         PSOUT%
         MOVX A,.CHRAB
         PBOUT%
       ELSE.
         SKIPE MBXPAG+300      ; some answer must be returned
         IFSKP.
           TMSG <451 Mailbox lookup process returned null answer>
           JRST GETCMD
         ENDIF.
         MOVEI D,MBXPAG+300    ; pointer to list of addresses
         DO.
           SKIPN C,(D)         ; if end of list, return
            EXIT.
           SKIPN 1(D)          ; is this the last item on the list?
            SKIPA A,[-1,,[ASCIZ/250 /]] ; yes, no continuation
             HRROI A,[ASCIZ/250-/] ; no, indicate continuation coming
           PSOUT%              ; output reply code and opening broket
           MOVX A,.CHLAB       ; MACRO still sucks after all these years
           PBOUT%
           TXNN C,.RHALF       ; local user reply?
            MOVSS C            ; yes, set up as local address reply
           HRRZ A,C            ; get user address
           CALL INFOUT         ; output string from inferior
           MOVX A,"@"          ; output mailbox/host delimiter
           PBOUT%
           IFXE. C,.LHALF      ; was a host specified?
             HRROI A,LCLHST    ; no, output local host name
             PSOUT%
           ELSE.
             HLRZ A,C          ; use specified host name
             CALL INFOUT       ; output string from inferior
           ENDIF.
           MOVX A,.CHRAB
           PBOUT%
           SKIPN 1(D)          ; is this the last item on the list?
           IFSKP. <TMSG <
>>                              ; no, output CRLF (don't use CALL CRLF!!)
           AOJA D,TOP.         ; continue until done
         ENDDO.
       ENDIF.
       JRST GETCMD

DOPTAB: PHASE 0                 ; delivery option names and F%DOP indices
D%MAIL:!ASCIZ/MAIL/             ; mail
D%SEND:!ASCIZ/SEND/             ; send
D%SOML:!ASCIZ/SOML/             ; send or mail
D%SAML:!ASCIZ/SAML/             ; send and mail
IFN <.-4>,<.FATAL Incorrect number of delivery options>
       DEPHASE

; SEND - initiate SEND transaction

SEND:   JUMPE C,MISARG          ; must have an argument
       JE F%HLO,,HLOREQ        ; bad sequence if HELO not done yet
       JN F%FRM,,INPROG        ; bad sequence if transaction already started
       MOVX A,D%SEND           ; set delivery option
       JRST MAKQUE             ; make a queued mail file

; SOML - initiate SEND transaction, mail if not on-line

SOML:   JUMPE C,MISARG          ; must have an argument
       JE F%HLO,,HLOREQ        ; bad sequence if HELO not done yet
       JN F%FRM,,INPROG        ; bad sequence if transaction already started
       MOVX A,D%SOML           ; set delivery option
       JRST MAKQUE             ; make a queued mail file

; SAML - initiate SEND transaction and mail

SAML:   JUMPE C,MISARG          ; must have an argument
       JE F%HLO,,HLOREQ        ; bad sequence if HELO not done yet
       JN F%FRM,,INPROG        ; bad sequence if transaction already started
       MOVX A,D%SAML           ; set delivery option
       JRST MAKQUE             ; make a queued mail file

; Table of devices to queue mail to

MLQTAB: -1,,[ASCIZ/MAILQ:/]     ; MAILQ: is the official directory
       -1,,[ASCIZ/SYSTEM:/]    ; if not, MMAILR still scans SYSTEM:
       -1,,[ASCIZ/DSK:/]       ; otherwise must use connected directory
MLQTBL==.-MLQTAB

; MAIL - initiate MAIL transaction

MAIL:   JUMPE C,MISARG          ; must have an argument
       JE F%HLO,,HLOREQ        ; bad sequence if HELO not done yet
       JN F%FRM,,INPROG        ; bad sequence if transaction already started
       MOVX A,D%MAIL           ; set delivery option
;       JRST MAKQUE             ; make a queued mail file

; Make a mailer queued request file

MAKQUE: STOR A,F%DOP            ; set delivery options
       MOVE A,BUFFER+1         ; get what comes after MAIL<SP>
       ANDCM A,[BYTE (7) 040,040,040,040,000] ; uppercaseify if needed
       CAME A,[ASCII/FROM:/]   ; was it MAIL FROM:, etc.?
        JRST SYNERR            ; no, syntax error
       MOVE A,[POINT 7,BUFFER+2] ; start parse after the colon
       TQO F%NOK               ; allow null mailbox
       TQZ F%MOK               ; if mailbox non-null, must have domain
       CALL PARMBX             ; parse a mailbox
        JRST SYNFLD            ; syntax error in mailbox
       IFN. C                  ; extended mail?
         CAXE C,.CHSPC
         IFSKP.
           ILDB C,A            ; stupid check for SIZE=
           CAIE C,"S"
            CAIN C,"s"
         ANNSK.
           ILDB C,A
           CAIE C,"I"
            CAIN C,"i"
         ANNSK.
           ILDB C,A
           CAIE C,"Z"
            CAIN C,"z"
         ANNSK.
           ILDB C,A
           CAIE C,"E"
            CAIN C,"e"
         ANNSK.
           ILDB C,A
           CAIE C,"="
         ANSKP.
           MOVEI C,^D10        ; read the size
           NIN%
         ANNJE.
           CAXG B,MAXSIZE
           IFSKP.
             TMSG <552 Message too large: >
             JRST DMPCMD
           ENDIF.
           LDB C,A             ; make sure command ends here
         ANDE. C
         ELSE.
           JRST SYNFLD
         ENDIF.
       ENDIF.
       MOVSI D,-MLQTBL         ; pointer to table of mail queue devices
       DO.
         HRROI A,TMPBUF        ; pointer to name of queued mail file we build
         MOVE B,MLQTAB(D)      ; get device to try
         SETZ C,
         SOUT%
         HRROI B,[ASCIZ/[--QUEUED-MAIL--].NEW-/]
         SOUT%                 ; set up initial part of name
         PUSH P,A              ; save string pointer
         GTAD%                 ; get system date/time
         MOVE B,A              ; now output it in octal
         POP P,A
         MOVX C,^D8
         NOUT%
          ERCAL FATAL
         HRROI B,[ASCIZ/-MAISER-J/] ; add originating process name
         SETZ C,
         SOUT%
         HRRZ B,MYJOBN         ; insert job number for unique name
         MOVX C,^D10           ; in decimal
         NOUT%
          ERCAL FATAL
         HRROI B,[ASCIZ/.-1;P770000/] ; next generation, protection 770000
         SETZ C,
         SOUT%
         MOVX A,GJ%NEW!GJ%FOU!GJ%PHY!GJ%SHT ; want new file
         HRROI B,TMPBUF        ; with name we build
         GTJFN%                ; try to get JFN on it
         IFJER.
           AOBJN D,TOP.        ; can't do it, try alternative place
           TMSG <421-Unable to get queue file - >
           CALL ERROUT         ; output last JSYS error
           JRST IMPERR         ; now die
         ENDIF.
         MOVEM A,MLQJFN        ; save JFN for later use
         MOVX B,<<FLD 7,OF%BSZ>!OF%WR> ; open for write, 7-bit bytes
         OPENF%
         IFJER.
           MOVE A,MLQJFN       ; OPENF% failed, release the JFN
           RLJFN%
            ERJMP .+1
           SETZM MLQJFN        ; forget about it
           AOBJN D,TOP.        ; can't do it, try alternative place
           TMSG <421-Unable to open queue file - >
           CALL ERROUT         ; output last JSYS error
           JRST IMPERR         ; now die
         ENDIF.
       ENDDO.
       SETZ C,                 ; make C be 0 for SOUT%'ing below
       SKIPN FRNHST            ; foreign host number known?
       IFSKP.
         MOVX B,.CHFFD         ; yes, write a NET-MAIL-FROM-HOST line
         BOUT%
          ERCAL FATAL
         HRROI B,[ASCIZ/=NET-MAIL-FROM-HOST:/]
         SOUT%
          ERCAL FATAL
         HRROI B,FRNHST        ; output host name
         SOUT%
          ERCAL FATAL
         HRROI B,[ASCIZ/
/]                              ; output trailing CRLF
         SOUT%
          ERCAL FATAL
       ENDIF.
       MOVX B,.CHFFD           ; write delivery options line
       BOUT%
        ERCAL FATAL
       HRROI B,[ASCIZ/=DELIVERY-OPTIONS:/]
       SOUT%
        ERCAL FATAL
       LOAD B,F%DOP            ; get delivery options
       HRROI B,DOPTAB(B)
       SOUT%
        ERCAL FATAL
       HRROI B,[BYTE (7) .CHCRT,.CHLFD,.CHFFD]
       SOUT%
        ERCAL FATAL
       SKIPE MAILBX            ; was a proper return path specified?
       IFSKP.
         HRROI B,[ASCIZ/=DISCARD-ON-ERROR/]
         SOUT%                 ; no, failures go to a black hole
       ELSE.
         HRROI B,[ASCIZ/=RETURN-PATH:/]
         SOUT%
          ERCAL FATAL
IFE FT2821,<    ; forbidden in RFC 2821
         SKIPN ATDOML          ; is an at-domain-list defined?
         IFSKP.
           HRROI B,ATDOML
           SOUT%
            ERCAL FATAL
         ENDIF.
>;IFE FT2821
         MOVE B,[POINT 7,MAILBX] ; now output Mailbox
         CALL MBXOUT
         MOVX B,"@"            ; mailbox/domain delimiter
         BOUT%
          ERCAL FATAL
         HRROI B,DOMAIN        ; output domain
         SOUT%
          ERCAL FATAL
         HRROI B,[BYTE (7) .CHCRT,.CHLFD,.CHFFD,"_"]
         SOUT%                 ; write sender specification
          ERCAL FATAL
         HRROI B,DOMAIN        ; output domain
         SOUT%
          ERCAL FATAL
         HRROI B,[BYTE (7) .CHCRT,.CHLFD]
         SOUT%
          ERCAL FATAL
         HRROI B,MAILBX        ; output mailbox
         SOUT%
          ERCAL FATAL
       ENDIF.
       HRROI B,[BYTE (7) .CHCRT,.CHLFD,.CHFFD]
       SOUT%
        ERCAL FATAL
       TQO F%FRM               ; flag "from" part of transaction complete
       TMSG <250 >             ; acknowlege command
       LOAD A,F%DOP            ; get delivery options
       HRROI A,DOPTAB(A)
       PSOUT%
       TMSG < accepted>
IFN FT2821,<
       SKIPN ATDOML
       IFSKP. <TMSG <, source route discarded per RFC 2821>>
>;IFN FT2821
       JRST GETCMD             ; get next command

; RCPT - identify a RECIPIENT for this transaction

RCPT:   JUMPE C,MISARG          ; must have an argument
       JE F%FRM,,MAIREQ        ; bad sequence if transaction not started yet
       MOVE A,BUFFER+1         ; get what comes after RCPT<SP>
       ANDCM A,[BYTE (7) 040,040,000,177,177] ; uppercaseify if needed
       CAME A,[ASCII/TO:/]     ; was it RCPT TO:?
        JRST SYNERR            ; no, syntax error
       MOVE A,[POINT 7,BUFFER+1,20] ; start parse after the colon
       TQZ F%NOK               ; do not allow null mailbox
       TQO F%MOK               ; if domain null, assume local host
       CALL PARMBX             ; parse a mailbox
        JRST SYNFLD            ; syntax error
       JUMPN C,SYNFLD          ; extended RCPT not permitted
;;; Reduce mailbox/domain name so that domain is NIL iff the address is truly
;;; local (with no "%" in the mailbox).
       DO.
         SKIPE DOMAIN          ; domain given?
         IFSKP.
           HRROI A,MAILBX      ; no domain specified, see if postmaster
           HRROI B,[ASCIZ/POSTMASTER/]
           STCMP%
           IFN. A
IFN FTSTALL,<
             MOVX A,^D3000     ; stall hackers
             DISMS%
>;IFN FTSTALL
             JRST SYNERR
           ENDIF.
         ELSE.
           HRROI A,DOMAIN      ; domain given, look up recipient host name
           SETO C,             ; through all naming registries
           CALL $GTPRO         ; get address and registry
           IFNSK.
IFN FTSTALL,<
             MOVX A,^D3000     ; stall hackers
             DISMS%
>;IFN FTSTALL
             TMSG <550 Host name ">
             HRROI A,DOMAIN    ; output the bad host
             PSOUT%
             TMSG <" unknown, recipient rejected>
             JRST GETCMD
           ENDIF.
           MOVE D,B            ; save address
           HRROI A,BUFFER      ; store local name out of the way
           SETO B,             ; want local address for this protocol
           CALL $GTNAM         ; get local name
           IFNSK.
             TMSG <421-Unable to get local host for recipient naming registry>
             JRST IMPERR
           ENDIF.
           CAME B,D            ; was destination host in fact us?
         ANSKP.
           MOVE A,[POINT 7,MAILBX] ; see if local mailbox wants to relay
           SETZ B,
           DO.
             ILDB C,A          ; sniff through mailbox looking for evil
             CAIE C,"%"
              CAIN C,"@"
               MOVE B,A        ; remember last "%" or "@"
             JUMPN C,TOP.
           ENDDO.
           IFN. B              ; saw a relay within local mailbox?
             DPB C,B           ; yes, snip off the relay name
             MOVE A,[POINT 7,DOMAIN] ; now copy relay name to domain
             DO.
               ILDB C,B
               IDPB C,A
               JUMPN C,TOP.
             ENDDO.
             LOOP.             ; reexamine the name
           ENDIF.
           SETZM DOMAIN        ; yes, note local domain
         ENDIF.
       ENDDO.
       SKIPE DOMAIN            ; local domain?
       IFSKP.
         LOAD A,F%DOP          ; get delivery option
         CAXE A,D%SEND         ; SEND?
         IFSKP.
           MOVX A,RC%EMO       ; yes, see if local user name
           HRROI B,MAILBX
           RCUSR%
           IFJER.
IFN FTSTALL,<
             MOVX A,^D3000     ; stall hackers
             DISMS%
>;IFN FTSTALL
             TMSG <550-Invalid username ">
             HRROI A,MAILBX    ; output the bad mailbox
             PSOUT%
             TMSG <", recipient rejected
550 Use SOML if you're trying to do a third-party send>
             JRST GETCMD
           ENDIF.
           IFXN. A,RC%NOM!RC%AMB ;Parsed, does it exist?
IFN FTSTALL,<
             MOVX A,^D3000     ; stall hackers
             DISMS%
>;IFN FTSTALL
             TMSG <550-No such local user as ">
             HRROI A,MAILBX    ; output the bad mailbox
             PSOUT%
             TMSG <", recipient rejected
550 Use SOML if you're trying to send to a mailing list>
             JRST GETCMD
           ENDIF.
           TQZ F%RFS           ; no online users refusing sends yet
           MOVX D,1            ; initial job number for scan
           MOVE E,C            ; user number to look for in E
           DO.
             MOVEI A,(D)       ; job number to sniff at
             MOVE B,[-<.JIBAT-.JITNO+1>,,GTJBLK]
             MOVX C,.JITNO     ; get TTY #, user #, ..., batch flag
             GETJI%
             IFJER.
               CAXN A,GTJIX4   ; No such job?
                AOJA D,TOP.    ; yes, try next higher job number
               TMSG <450 User ">
               HRROI A,MAILBX  ; output the bad mailbox
               PSOUT%
               TQNE F%RFS      ; was there an online job refusing?
                SKIPA A,[-1,,[ASCIZ/" is refusing sends/]]
                 HRROI A,[ASCIZ/" is not online now/]
               PSOUT%
               TMSG <, try again later>
               JRST GETCMD
             ENDIF.
             SKIPE GTJBLK+<.JIBAT-.JITNO> ; is this a batch job?
              AOJA D,TOP.      ; yes, skip it
             SKIPL A,GTJBLK    ; attached to a terminal
              CAME E,GTJBLK+<.JIUNO-.JITNO> ; yes, the user we want?
               AOJA D,TOP.     ; no to either, try next job
             TXO A,.TTDES      ; make it a device designator
             MOVX B,.MORNT     ; does user want system messages?
             MTOPR%
             IFNJE.
               JUMPE C,ENDLP.  ; found a logged in user receiving sends, done!
             ENDIF.
             TQO F%RFS         ; found an online user who's refusing
             AOJA D,TOP.       ; otherwise try next job
           ENDDO.
         ELSE.
           SKIPE $ASRCP        ; OK to validate address in RCPT?
         ANSKP.
           TQZ F%EXP           ; yes, don't expand here
           HRROI A,MAILBX
           CALL RUNMBX         ; validate address
         ANNSK.
           SKIPE MBXFRK        ; failed, did mailbox fork run successfully?
           IFSKP.
             TMSG <451 Mailbox lookup process terminated abnormally>
             JRST GETCMD
           ENDIF.
           SKIPG MBXFRK        ; is there a mailbox fork?
         ANSKP.
IFN FTSTALL,<
           MOVX A,^D3000       ; stall hackers
           DISMS%
>;IFN FTSTALL
           TMSG <550 No such local mailbox as ">
           HRROI A,MAILBX      ; output the bad mailbox
           PSOUT%
           TMSG <", recipient rejected>
           JRST GETCMD
         ENDIF.
       ELSE.
         LOAD A,F%PRO          ; non-local get connection protocol
         CAXE A,P%TCP          ; is it TCP?
       ANSKP.
         CALL LCLCHK           ; is foreign host local domain?
       ANSKP.
         HLRO A,(C)            ; not local domain, get destination registry
         HRROI B,[ASCIZ/TCP/]
         STCMP%                ; TCP destination?
         IFE. A
IFN FTSTALL,<
           MOVX A,^D3000       ; stall hackers
           DISMS%
>;IFN FTSTALL
           TMSG <550 Destination not local, recipient rejected>
           JRST GETCMD
         ENDIF.
         HLRO A,(C)            ; see if MX name
         HRROI B,[ASCIZ/MX/]
         STCMP%
       ANDE. A
         HRROI A,DOMAIN        ; MX name, are we a relay for it?
         CALL RLYCHK
       ANSKP.
IFN FTSTALL,<
         MOVX A,^D3000         ; stall hackers
         DISMS%
>;IFN FTSTALL
         TMSG <550 Invalid relay, recipient rejected>
         JRST GETCMD
       ENDIF.
       SKIPE A,MLQJFN          ; get JFN of queue file
       IFSKP.
         TMSG <421-Queue not set up in RCPT command>
         JRST IMPERR
       ENDIF.
       SKIPN DOMAIN            ; domain specified?
        SKIPA B,[-1,,LCLHST]   ; no, use local host as default domain
         HRROI B,DOMAIN        ; output destination domain
       SETZ C,
       SOUT%
        ERCAL FATAL
       HRROI B,[ASCIZ/
/]
       SOUT%
        ERCAL FATAL
       HRROI B,MAILBX          ; now output destination mailbox
       SOUT%
        ERCAL FATAL
       HRROI B,[BYTE (7) .CHCRT,.CHLFD,.CHFFD]
       SOUT%
        ERCAL FATAL
       TQO F%TO                ; flag "to" part of transaction complete
       TMSG <250 Recipient accepted> ; acknowledge
       JRST GETCMD             ; and get next command

; DATA - DATA for mail transaction

DATA:   JUMPN C,BADARG          ; must not have an argument
       JE F%TO,,RCPREQ         ; have FROM/TO specifications?
       SETZ H,                 ; initially no bytes in message
       SKIPE A,MLQJFN          ; get JFN of queue file
       IFSKP.
         TMSG <421-Queue not set up in DATA command>
         JRST IMPERR
       ENDIF.
       LOAD B,F%DOP            ; get delivery option
       CAXN B,D%SEND           ; if SEND, don't add Received: header
       IFSKP.
         HRROI B,[ASCIZ/
Received: from /]               ; now, write Received line
         SETZ C,
         SOUT%
          ERCAL FATAL
         HRROI B,FRNHNM        ; write foreign host
         SOUT%
          ERCAL FATAL
         HRROI B,[ASCIZ/ (/]   ; start a comment
         SOUT%
          ERCAL FATAL
         LOAD D,F%PRO          ; get connection protocol
         CAXE D,P%TCP          ; is it TCP?
         IFSKP.
           IFQE. F%VLH         ; yes, foreign host number validated?
             HRROI A,FRNHST    ; no, do we have a name from reverse lookup?
             CALL $GTHRL
           ANNSK.
             MOVE A,MLQJFN     ; have a name (as opposed to domain literal)
             HRROI B,FRNHST    ; output foreign host name
             SOUT%
              ERCAL FATAL
             MOVX B,.CHSPC     ; delimit with space
             BOUT%
              ERCAL FATAL
           ENDIF.
           MOVE A,[POINT 7,BUFFER]
           MOVE B,FRNHNO
           CALL $GTHWL         ; get domain literal
           SETZ B,             ; tie off literal
           IDPB B,A
           MOVE A,MLQJFN       ; write domain literal
           HRROI B,BUFFER
           SOUT%
            ERCAL FATAL
           IFQN. F%NAH         ; warn if forged
             HRROI B,[ASCIZ/ -- may be forged/]
             SOUT%
              ERCAL FATAL
           ENDIF.
         ELSE.
           IFQE. F%VLH         ; foreign host number validated?
             SKIPN FRNHST      ; no, real foreign host known?
              SKIPA B,[-1,,[ASCIZ/not validated/]]
               HRROI B,FRNHST  ; output foreign host name
             SOUT%
              ERCAL FATAL
           ENDIF.
         ENDIF.
         HRROI B,[ASCIZ/) by /]
         SOUT%
          ERCAL FATAL
         HRROI B,LCLHST        ; write local host
         SOUT%
          ERCAL FATAL
         HRROI B,[ASCIZ/; /]   ; default is no With specification
         LOAD D,F%PRO          ; get protocol used
         CAXN D,P%TCP          ; TCP?
          HRROI B,[ASCIZ" with TCP/SMTP; "]
         CAXN D,P%CHA          ; Chaos?
          HRROI B,[ASCIZ" with Chaos/SMTP; "]
         SOUT%
          ERCAL FATAL
         SETO B,               ; output current date/time
         MOVX C,OT%DAY!OT%SPA!OT%TMZ!OT%SCL!OT%822 ; RFC 822 standard daytime
         ODTIM%
          ERCAL FATAL
       ENDIF.
       HRROI B,[ASCIZ/
/]                              ; now output terminating CRLF
       SETZ C,
       SOUT%
        ERCAL FATAL
       TMSG <354 Start mail input; end with <CRLF>.<CRLF>>
       CALL CRLF
       TQZ F%EOL               ; no EOL seen on this line yet
       SETO E,                 ; no lookahead yet
       DO.
         MOVNI A,TIMOCT        ; reset timeout count
         MOVEM A,TIMOUT
         MOVE B,[POINT 7,BUFFER] ; pointer to buffer
         MOVX C,TXTLEN-1       ; up to this many characters
         SKIPGE A,E            ; any lookahead byte?
         IFSKP.
           SETO E,             ; yes, no lookahead now
           IDPB A,B            ; stash it in the buffer
           SUBI C,1            ; account for it
           CAXE A,.CHCRT       ; was it a CR?
         ANSKP.                ; if so don't read anything
         ELSE.
           MOVX A,.PRIIN       ; read a line from primary input
IFE FTDATABUG,<
           MOVX D,.CHCRT       ; terminate on carriage return
>;IFE FTDATABUG
IFN FTDATABUG,<
           MOVX D,.CHLFD       ; terminate on line feed
>;IFN FTDATABUG
           SIN%
            ERJMP INPEOF       ; finish up on error
           LDB A,B             ; get last character read
         ENDIF.
IFE FTDATABUG,<
         CAXE A,.CHCRT         ; was it a CR?
         IFSKP.
           PBIN%               ; yes, get byte after CR
            ERJMP INPEOF       ; finish up on error
           CAXE A,.CHLFD       ; is this a real EOL?
           IFSKP.
             IDPB A,B          ; yes, insert it in the buffer
             SUBI C,1          ; account for it in the buffer
             TQO F%EOL         ; flag EOL seen
           ELSE.
             MOVE E,A          ; set lookahead byte after CR
           ENDIF.
         ENDIF.
>;IFE FTDATABUG
IFN FTDATABUG,<
;;;  This code is in direct violation of explicit text in RFC 2821 that forbids
;;; this behavior.  What's worse, it creates a loophole for spammers.  Don't do
;;; it.
         CAXE A,.CHLFD         ; was it a LF?
         IFSKP.
           TQO F%EOL           ; flag EOL seen
           SETO D,             ; point to character before LF
           ADJBP D,B
           LDB A,D             ; get that character
           CAXN A,.CHCRT       ; was it a CR?
           IFSKP.
             MOVX A,.CHCRT     ; no, overwrite LF with CRLF
             IDPB A,D
             MOVX A,.CHLFD
             IDPB A,D
             SUBI C,1          ; account for it in the buffer
           ENDIF.
>;IFN FTDATABUG
         MOVE B,[POINT 7,BUFFER] ; buffer we read into
         SUBI C,TXTLEN-1       ; negative count of bytes to output
         IFQN. F%ELP           ; buffer begin with EOL?
           LDB A,[POINT 7,BUFFER,6] ; yes, get first byte of buffer
           CAIE A,"."          ; was it a period?
           IFSKP.
             IBP B             ; yes, skip over it
             ADDI C,1          ; account for it in the count
             IFQN. F%EOL       ; buffer end with EOL?
               CAMN C,[-2]     ; yes, only two bytes to output?
                EXIT.          ; yes, must be EOM
             ENDIF.
           ENDIF.
         ENDIF.
         MOVE A,MLQJFN         ; output buffer to queue file
         CAXLE H,MAXSIZE       ; already exceeded limit?
         IFSKP.
           MOVM G,C            ; get number of bytes to write
           ADD H,G             ; count this many bytes
           CAXG H,MAXSIZE      ; message too large?
            SOUT%              ; no - OK to write
             ERCAL FATAL
         ENDIF.
         TQZE F%EOL            ; EOL seen?
          TQOA F%ELP           ; yes, set EOL seen in previous buffer
           TQZ F%ELP           ; no EOL in previous buffer
         LOOP.
       ENDDO.
       SETZM TIMOUT            ; can't time out now
       CAXG H,MAXSIZE          ; message too large?
       IFSKP.
         TMSG <552 Message too large>
         JRST RSET2            ; abort message
       ENDIF.
       MOVE A,MLQJFN           ; yes, must be EOM
       CLOSF%
        ERCAL FATAL
       SETZM MLQJFN            ; flush the JFN
       TMSG <250-Message accepted and queued for delivery
>
       CALL $WAKE              ; wake up MMailr
       JRST RSET1              ; now do an implicit RSET

; QUIT - QUIT out of mail service

QUIT:   JUMPN C,BADARG          ; must not have an argument
       TMSG <221 >             ; start acknowledgement
QUIT1:  HRROI A,LCLHST          ; output our host name
       PSOUT%
       TMSG < Service closing transmission channel>
QUIT2:  CALL CRLF
INPEOF: CALL HANGUP             ; hang up the connection
       JRST MAISER             ; restart program

HANGUP: SETZM TIMOUT            ; can't time out now
       MOVE A,[.FHSLF,,.TIMAL] ; remote all pending timers
       TIMER%
        ERCAL FATAL
       SKIPN A,MLQJFN          ; if a queue file open, flush its JFN
       IFSKP.
         TXO A,CZ%ABT          ; abort it
         CLOSF%
          ERJMP .+1            ; why should this fail?
         SETZM MLQJFN          ; flush JFN
       ENDIF.
       MOVX A,.PRIOU           ; wait until the output happens
       DOBE%
        ERJMP .+1
       IFQN. F%NVT             ; NVT server?
         DTACH%                ; detach the job to prevent "Killed..." message
          ERJMP .+1
         SETO A,               ; now log myself out
         LGOUT%
          ERJMP .+1
       ENDIF.
       IFQN. F%JFN             ; JFN that needs closing?
         MOVX A,.FHSLF         ; yup, find out what the JFNs were
         GPJFN%
          ERJMP .+1
         MOVE D,B              ; save returned value
         SETO B,               ; set primary I/O back to default value
         SPJFN%                ; so that we can close the JFNs
          ERJMP .+1
         HLRZ A,D              ; now close the JFNs
         CLOSF%
          ERJMP .+1            ; not much we can do if this fails
         MOVS A,D              ; don't try to close the same JFN twice,
         CAMN A,D              ;  it belong to some other fork by now!
       ANSKP.                  ; JFNs weren't the same, so close .PRIOU
         HRRZ A,D              ; close the other JFN
         CLOSF%
          ERJMP .+1
       ENDIF.
       HALTF%                  ; stop
       RET

; NOOP - NOOP null command

NOOP:   JUMPN C,BADARG          ; must not have an argument
       TMSG <250 OK>           ; acknowledge command
       JRST GETCMD

; HELP - HELP message

HELP:   JUMPN C,BADARG          ; must not have an argument
       HRROI A,HLPMSG          ; output help message
       PSOUT%
       JRST GETCMD

HLPMSG: ASCIZ/214-The following commands are implemented:
214- EHLO, HELO, MAIL, RCPT, DATA, RSET, NOOP, QUIT, SEND, SOML, SAML,
214- VRFY, EXPN, HELP, TURN
214 This system is running the TOPS-20 operating system/

; TURN - TURN around transaction

TURN:   JUMPN C,BADARG          ; must not have an argument
       TMSG <250 TURN command accepted, send 220 greeting>
       CALL CRLF
       CALL RDRPLY             ; read SMTP reply
       CAME A,[ASCII/220/]     ; 220 greeting?
       IFSKP.
         TMSG <HELO >          ; yes, output HELO
         HRROI A,LCLHST        ; and local host name
         PSOUT%
         CALL CRLF
         CALL RDRPLY
; *** Here would go code to support a future implementation of outgoing mail.
; The purpose of this is for situations where two-way mail interactions on
; the same connection are useful.
       ENDIF.
       CAMN A,[ASCII/421/]     ; was last reply code a 421 hangup?
       IFSKP.
         TMSG <QUIT>
         CALL CRLF             ; no, negotiate a normal QUIT
         CALL RDRPLY           ; get reply for it
       ENDIF.
       CALL HANGUP             ; hang up the connection
       JRST MAISER             ; restart

;  Read SMTP reply from server process (for TURN command).  Returns ASCII
; of reply code in A.

RDRPLY: DO.
         SETZM BUFFER          ; make sure no random crud here
         MOVX A,.PRIIN         ; from primary input
         HRROI B,BUFFER        ; pointer to command buffer
         MOVX C,TXTLEN-1       ; up to this many characters
         MOVX D,.CHCRT         ; terminate on carriage return
         SIN%                  ; read the greeting header
          ERJMP INPEOF         ; finish up on error
         LDB A,B               ; get last byte of line
         DO.                   ; slurp up bytes until see a CRLF
           CAXN A,.CHCRT       ; got a CR?
           IFSKP.
             PBIN%             ; no, read next byte
              ERJMP INPEOF     ; finish up on error
             LOOP.             ; see if this one looks good
           ENDIF.
           PBIN%               ; get expected LF
            ERJMP INPEOF       ; finish up on error
           CAXE A,.CHLFD       ; saw LF?
            LOOP.              ; no, start over again
         ENDDO.
         LDB A,[POINT 7,BUFFER,27] ; get possible continuation byte
         CAXN A,.CHHYP         ; was continuation specified?
          LOOP.                ; yes, get new line
         CAXE A,.CHSPC         ; single reply seen?
          CALL HANGUP          ; no, something's wrong - punt
       ENDDO.
       MOVE A,BUFFER           ; get reply code
       AND A,[BYTE (7) 177,177,177,000,000] ; without text crud
       RET                     ; return to caller
      SUBTTL Subroutines

;  Here to parse a mailbox specification pointed to in A.  Skips if success.
; Returns a-d-l in ATDOML, mailbox in MAILBX, and domain in DOMAIN.
; F%NOK indicates that a null mailbox is allowed, to allow null return-paths
; per the SMTP specification.
; F%MOK indicates that a domain is optional, that is, the command:
;       RCPT TO:<FOO>
; will be interpreted as local mailbox FOO.

PARMBX: SETZM MBXBEG            ; clear previous mailbox
       MOVE C,[MBXBEG,,MBXBEG+1]
       BLT C,MBXEND
       ILDB C,A                ; get opening character
       CAXE C,.CHLAB           ; must be opening broket
        RET                    ; parse fails
       ILDB C,A                ; get first character in path
       CAXE C,.CHRAB           ; is this a close broket?
       IFSKP.
         JN F%NOK,,PRMDUN      ; yes, if null mailbox okay then return success
       ENDIF.
       CAIE C,"@"              ; a-d-l present?
       IFSKP.
         MOVE B,[POINT 7,ATDOML] ; set up pointer to a-d-l
         IDPB C,B              ; store the starting "@"
         MOVX D,ADLLEN-1       ; set up limit of domain list length
         DO.
           CALL GETDOM         ; get a domain
            RET                ; syntax error in domain
           CAIE C,","          ; another domain in route list?
           IFSKP.
             IDPB C,B          ; yes, save the comma
             SOJL D,R          ; count the comma
             ILDB C,A          ; get next byte
             CAIE C,"@"        ; start of next at-domain?
             IFSKP.
               IDPB C,B        ; yes, store this "@"
               SOJGE D,TOP.    ; count the "@"
               RET             ; no more space
             ENDIF.
             MOVX D,":"        ; no, must be an RFC 788 SMTP sender, patch
             DPB D,B           ;  a colon over the comma and exit
           ELSE.
             CAIE C,":"        ; end of domain?
              RET              ; no, syntax error in domain
             IDPB C,B          ; save a-d-l terminator
             SOJL D,R          ; let's count that terminator as well
             ILDB C,A          ; get first character of local part
           ENDIF.
         ENDDO.
       ENDIF.

; Here to process the local part of a mailbox, C has first character

       MOVE B,[POINT 7,MAILBX] ; set up pointer to mailbox
       MOVX D,USRNML           ; set up maximum length of user name
       CAXE C,.CHQOT           ; quoted string?
       IFSKP.
         DO.
           ILDB C,A            ; yes, get next quoted byte
           CAXE C,.CHQOT       ; end of quoted string?
           IFSKP.
             ILDB C,A          ; get expected at
             CAIN C,"@"        ; was it an at?
              EXIT.            ; saw an at, finished with mailbox
             CAXN C,.CHRAB     ; is this a close broket?
              SKIPN MAILBX     ; yes, was mailbox non-null?
               RET             ; not close broket or mailbox null, syntax err
             JN F%MOK,,PRMDUN  ; yes, if F%MOK then allow missing domain
             RET               ; syntax error
           ENDIF.
           CAXE C,.CHCRT       ; CR or LF invalid in quoted string
            CAXN C,.CHLFD
             RET
           CAIN C,"\"          ; quote next byte literally?
            ILDB C,A           ; yes, get next byte
           IDPB C,B            ; store byte in string
           SOJGE D,TOP.        ; continue with next byte unless overflowed
           RET                 ; mailbox name too long
         ENDDO.
       ELSE.
         DO.                   ; parse unquoted string
           MOVEI E,(C)         ; get copy of character
           IDIVI E,^D32        ; E/ word to check, F/bit to check
           MOVNS F
           MOVX G,1B0          ; make bit to check
           LSH G,(F)
           TDNE G,SPCMSK(E)    ; is it a special character?
            RET                ; yes, syntax error
           CAXE C,.CHRAB       ; saw close broket?
           IFSKP.
             SKIPN MAILBX      ; yes, was mailbox non-null?
              RET              ; no, syntax error
             JN F%MOK,,PRMDUN  ; if F%MOK then allow missing domain
             RET               ; else syntax error
           ENDIF.
           CAIN C,"@"          ; was it an at?
           IFSKP.
             CAIN C,"\"        ; quote next byte literally?
              ILDB C,A         ; yes, get next byte
             IDPB C,B          ; store byte in string
             ILDB C,A          ; get next byte to consider
             SOJGE D,TOP.      ; continue byte unless overflowed
             RET
           ENDIF.
         ENDDO.
       ENDIF.

; Process the destination domain and terminate the command string

       MOVE B,[POINT 7,DOMAIN] ; point at domain string
       MOVX D,HSTNML           ; maximum length of a host name
       CALL GETDOM             ; get domain name
        RET                    ; syntax error in domain
       CAXE C,.CHRAB           ; closing broket?
        RET                    ; no, syntax error
       SKIPE MAILBX            ; mailbox required
        SKIPN DOMAIN           ; domain required
         RET                   ; mailbox or domain missing
PRMDUN: ILDB C,A                ; get line ending character
       RETSKP

; Table of special characters

       BRINI.                  ; initialize break mask

       BRKCH. (.CHNUL,.CHSPC)  ; all controls are special characters
       BRKCH. (042)            ; """"
       BRKCH. (050,051)        ; "(", ")"
       BRKCH. (054)            ; ","
       BRKCH. (072,074)        ; ":", ";", "<"
;       BRKCH. (076)            ; ">" commented out because processed in code
;       BRKCH. (100)            ; "@" commented out because processed in code
       BRKCH. (133)            ; "["
;       BRKCH. (134)            ; "\" commented out because processed in code
       BRKCH. (135)            ; "]"

SPCMSK: EXP W0.,W1.,W2.,W3.     ; form table of special characters

; These tables are for quoting in the return-path

       BRINI.                  ; initialize break mask

       BRKCH. (.CHCNA,.CHTAB)  ; CTRL/A through CTRL/I
       BRKCH. (.CHVTB,.CHFFD)  ; CTRL/K, CTRL/L
       BRKCH. (.CHCNN,.CHSPC)  ; CTRL/N through space
       BRKCH. (050,051)        ; "(", ")"
       BRKCH. (054)            ; ","
       BRKCH. (072,074)        ; ":", ";", "<"
       BRKCH. (076)            ; ">"
       BRKCH. (100)            ; "@"
       BRKCH. (133)            ; "["
       BRKCH. (135)            ; "]"

QOTMSK: EXP W0.,W1.,W2.,W3.

; If any of these characters are seen, they must be quoted with backslash

       BRINI.                  ; initialize break mask

       BRKCH. (.CHLFD)         ; line feed
       BRKCH. (.CHCRT)         ; carriage return
       BRKCH. (042)            ; """"
       BRKCH. (134)            ; "\"

QT1MSK: EXP W0.,W1.,W2.,W3.

;  Here to get a domain string, source pointer in A, destination pointer in B,
; maximum number of bytes in D.  Skips if success with delimiter in C.

GETDOM: ILDB C,A                ; get first byte of domain string
       CAIE C,"#"              ; monolithic number?
       IFSKP.
         IDPB C,B              ; save indicator of moby number
         SUBI D,1              ; account for character
         ILDB C,A              ; get first byte of number
         CAIL C,"0"            ; is it a number?
          CAILE C,"9"
           RET                 ; must have at least one digit
         DO.
           IDPB C,B            ; save digit
           ILDB C,A            ; get subsequent digit(s)
           CAIL C,"0"          ; is it a number?
            CAILE C,"9"
             EXIT.             ; no, end of domain
           SOJGE D,TOP.        ; else store digit and try again
           RET                 ; string too long
         ENDDO.
       ELSE.
         CAIE C,"["            ; dot-number?
         IFSKP.
           MOVX E,3            ; number of dots expected in field
           DO.
             IDPB C,B          ; save bracket or dot
             SOJL D,R          ; account for character (syn err if full)
             ILDB C,A          ; get first byte of number
             CAIL C,"0"        ; is it a number?
              CAILE C,"9"
               RET             ; must have at least one digit
             DO.               ; collect a number into the buffer
               IDPB C,B        ; save digit
               ILDB C,A        ; get subsequent digit(s)
               CAIL C,"0"      ; is it a number?
                CAILE C,"9"
                 EXIT.         ; no, leave
               SOJGE D,TOP.    ; numeric, store digit and try again
               RET             ; string too long
             ENDDO.
             SOJL E,ENDLP.     ; if seen three dots then done
             CAIN C,"."        ; dot expected, did we see one?
              LOOP.            ; yes, store it and collect next number
             RET               ; else syntax error
           ENDDO.
           CAIE C,"]"          ; closing bracket?
            RET                ; no, syntax error
           IDPB C,B            ; store closing bracket in string
           SOJL D,R            ; see if it makes string too long
           ILDB C,A            ; get delimiter byte for caller
         ELSE.
           CAIL C,"A"          ; non-alphabetic?
            CAILE C,"z"
           IFSKP.
             CAILE C,"Z"       ; further alphabetic checking
              CAIL C,"a"
             IFSKP. <RET>      ; non-alphabetic, lose
           ELSE.
             CAIL C,"0"        ; numeric?
              CAILE C,"9"
               RET             ; non-numeric, lose
           ENDIF.
           DO.
             IDPB C,B          ; store byte in string
             SOJL D,R          ; length check
             ILDB C,A          ; get next byte of string
             CAIE C,"."        ; dot?
              CAXN C,.CHHYP    ; hyphen?
               LOOP.           ; yes, store in string
             CAIL C,"A"        ; non-alphabetic?
              CAILE C,"z"
             IFSKP.
               CAILE C,"Z"     ; further alphabetic checking
                CAIL C,"a"
                 LOOP.         ; character is alphabetic, store in string
             ENDIF.
             CAIL C,"0"        ; numeric?
              CAILE C,"9"
               EXIT.           ; no, end of domain
             LOOP.             ; character is numeric, store in string
           ENDDO.
           LDB E,B             ; get last byte in string
           CAIE E,"."          ; disallow null domain element
            CAXN E,.CHHYP      ; domain string may not end in hyphen
             RET               ; it did, syntax error
         ENDIF.
       ENDIF.
       SAVEAC <B>              ; leave string pointing at null
       SETZ E,                 ; tie off string with null
       IDPB E,B
       RETSKP                  ; return success to caller

; Here to lookup a mailbox pointed to in A in the mailbox database.  Skips
; if mailbox found, with pointers in MBXPAG+300.

RUNMBX: SAVEAC <A>              ; don't clobber mailbox pointer
       STKVAR <MBXPTR>
       MOVEM A,MBXPTR          ; save mailbox pointer
       SKIPLE MBXFRK           ; see if already a mailbox fork
       IFSKP.
         SETOM MBXFRK          ; no, flag trying to get a mailbox fork
         SETOM MBXWIN          ; clear memory of cached mailbox window
         MOVX A,GJ%OLD!GJ%SHT  ; get JFN of forwarder
         HRROI B,[ASCIZ/SYS:MMAILBOX.EXE/]
         GTJFN%
          ERJMP R              ; not implemented if no mailbox fork
         MOVEM A,MBXFRK        ; save here temporarily
         MOVX A,CR%CAP         ; create an inferior fork
         CFORK%
          ERCAL FATAL
         EXCH A,MBXFRK         ; save fork handle, get JFN
         HRL A,MBXFRK          ; get prog into fork
         GET%
          ERCAL FATAL
       ENDIF.
       HRLZ A,MBXFRK           ; page 0 of inferior
       DMOVE B,[.FHSLF,,MBXPAG/1000 ; mapped to this fork's MBXPAG
                PM%RD!PM%WR!PM%CNT+2] ; read+write access
       PMAP%
        ERCAL FATAL
       MOVE A,[POINT 7,MBXPAG+200] ; destination
       MOVE B,MBXPTR           ; source address
       MOVX C,-USRNML          ; maximum length of an address
       SOUT%
        ERCAL FATAL
       MOVE A,MBXFRK           ; get fork handle back again
       TQNN F%EXP              ; need to expand?
        SKIPA B,[4]            ; no, just verify existance
         MOVX B,3              ; expansion entry
       SFRKV%                  ; start fork
        ERCAL FATAL
       WFORK%                  ; wait for it to halt
        ERCAL FATAL
       RFSTS%                  ; see if it finished ok
        ERCAL FATAL
       HLRZ A,A
       CAXN A,.RFHLT           ; halted normally?
       IFSKP.
         SETO A,               ; unmap shared pages
         DMOVE B,[.FHSLF,,MBXPAG/1000 ; mapped to this fork's MBXPAG
                  PM%CNT+2]
         PMAP%
          ERCAL FATAL
         DMOVE B,[.FHSLF,,WINPAG/1000 ; mapped to this fork's WINPAG
                  PM%CNT+2]
         PMAP%
          ERCAL FATAL
         MOVE A,MBXFRK         ; flush the fork
         KFORK%
          ERCAL FATAL
         SETZM MBXFRK
         RET
       ENDIF.
       SKIPG MBXPAG+177        ; yes, success answer?
        RET                    ; no, non-skip return
       RETSKP                  ; success, skip return with fork still mapped

       ENDSV.

; Output string from mailbox starting from address in A

INFOUT: SAVEAC <A,B,C>          ; preserve ACs
       STKVAR <MBXADR>
       MOVEM A,MBXADR          ; save address we're going to PSOUT% for later
       LSH A,-<^D9>            ; get inferior page number desired
       CAMN A,MBXWIN           ; already cached?
       IFSKP.
         MOVEM A,MBXWIN        ; no, set as new mailbox window page
         DMOVE B,[.FHSLF,,WINPAG/1000 ; map two pages to our WINPAG
                  PM%CNT!PM%RD!PM%CPY+2]
         CAIN A,777            ; guard against page 777
          SUBI C,1             ; oops, only one page then
         HRL A,MBXFRK          ; mailbox fork,,page number
         PMAP%
          ERCAL FATAL
       ENDIF.
       MOVX A,.PRIOU           ; output to primary I/O
       MOVE B,MBXADR           ; get address back
       MOVX C,<WINPAG/1000>    ; page in our address space
       DPB C,[POINT 9,B,26]    ; set up as new address
       HRLI B,(<POINT 7,>)     ; make pointer
       CALLRET MBXOUT          ; output mailbox

       ENDSV.

; Here to output mailbox with RFC822 quoting
; Accepts: A/ destination designator
;          B/ mailbox source pointer
;       CALL MBXOUT
; Returns +1: always

MBXOUT: SAVEAC <C,D,E,F,G>
       STKVAR <SRCPTR>
       MOVEM B,SRCPTR          ; save source pointer
       TQZ F%QOT               ; initially require no quoting
       MOVX B,"\"              ; quote for wierd characters
       MOVE G,[POINT 7,TMPBUF] ; pointer to temporary buffer
       DO.                     ; copy to TMPBUF with \ insert and " need check
         ILDB C,SRCPTR         ; get character from source
          ERCAL FATAL          ; in case of page mapping lossage
         MOVEI E,(C)           ; make a copy of it to hack
         IDIVI E,^D32          ; E := word to check, F := bit to check
         MOVNS F
         MOVX D,1B0            ; D := bit to check
         LSH D,(F)
         TDNE D,QOTMSK(E)      ; is it a special character?
          TQO F%QOT            ; yes, note
         TDNE D,QT1MSK(E)      ; is it an wierd character?
          IDPB B,G             ; yes, put in wierd character quote
         IDPB C,G              ; now copy character
         JUMPN C,TOP.          ; continue
       ENDDO.
       MOVX B,.CHQOT
       TQNE F%QOT              ; need to do atomic quoting?
        BOUT%                  ; yes, insert it
       HRROI B,TMPBUF          ; output buffer
       SETZ C,
       SOUT%
       MOVX B,.CHQOT
       TQNE F%QOT              ; need to do atomic quoting?
        BOUT%                  ; yes, insert it
       RET

; Outputs a CRLF iff it is necessary

CRLF:   SAVEAC <A,B,C>
       MOVX A,.PRIOU           ; use SOUTR% for non-TTY primary I/O
       HRROI B,[ASCIZ/
/]
       SETZ C,
       SOUTR%                  ; this pushes the text on networks
        ERJMP .+1
       RET

; Convert a 32-bit quantity in A from squoze to ASCII

SQZTYO: IDIVI A,50              ; divide by 50
       PUSH P,B                ; save remainder, a character
       SKIPE A                 ; if A is now zero, unwind the stack
        CALL SQZTYO            ; call self again, reduce A
       POP P,A                 ; get character
       ADJBP A,[POINT 7,[ASCII/ 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ.$%/],6]
       LDB A,A                 ; convert squoze code to ASCII
       PBOUT%
       RET

; See if foreign host is in the same local domain

LCLCHK: SAVEAC <A,B>
       JN F%NAH,,RSKP          ; never local if not validated
       HRROI A,LCLHST          ; literal local name never same domain
       CALL $GTHRL
       IFNSK.
         HRROI A,FRNHST        ; ditto literal foreign name
         CALL $GTHRL
       ANNSK.
         MOVE A,[POINT 7,LCLHST] ; scan for second-level domain name
         CALL GETSLD
       ANDN. A
         MOVE B,A              ; note local second-level domain
         MOVE A,[POINT 7,FRNHST]
         CALL GETSLD
       ANDN. A
         STCMP%                ; compare two second-level domains
         JUMPE A,R             ; local if compare wins
       ENDIF.
       RETSKP                  ; not local

; Get second-level domain name pointer

GETSLD: SAVEAC <B,C,D,E>
       MOVE E,A                ; save original argument
       SETZB B,D               ; no previous pointers
       DO.
         ILDB C,A              ; get byte from name
         CAIE C,"."            ; found a domain element?
         IFSKP.
           MOVE D,B            ; yes, save previous pointer
           MOVE B,A            ; set new pointer
         ENDIF.
         JUMPN C,TOP.          ; loop until done
       ENDDO.
       SKIPN A,D               ; return pointer
        SKIPN B                ; no third-level domain, was there a second?
         RET
       MOVE A,E                ; name already is second-level domain
       RET

; See if we are an MX relay for a host

RLYMAX==^D10                    ; maximum relays
GTDLEN==.GTDML+RLYMAX           ; size of GTDOM% block
RLYBFL==RLYMAX*<<HSTNML/5>+1>   ; relay buffer

RLYCHK: SAVEAC <A,B,C,D,E>
       STKVAR <HOST,<GTDBLK,GTDLEN+1>,<RLYBUF,RLYBFL>>
       MOVEM A,HOST
       SETZM GTDBLK            ; init argument block
       MOVSI A,GTDBLK
       HRRI A,1+GTDBLK
       BLT A,GTDLEN+GTDBLK
       MOVX A,GTDLEN           ; block length
       MOVEM A,.GTDLN+GTDBLK
       MOVX A,<RLYBFL*5>-1     ; relay buffer length in chars
       MOVEM A,.GTDBC+GTDBLK
       MOVX A,.GTDMX           ; get MX
       MOVE B,HOST             ; host to check
       HRROI C,RLYBUF          ; relays written here
       MOVEI D,GTDBLK          ; argument block
       CALL $GTHST
       IFSKP.
         MOVEI E,.GTDRD+GTDBLK ; scan relay list
         DO.
           SKIPN A,(E)         ; get next relay
            EXIT.
           HRROI B,LCLHST      ; matches local host?
           STCMP%
           JUMPE A,R
           MOVX A,.GTDAA       ; see if this is a valid name for us
           MOVE B,(E)
           SETO C,             ; on any of my addresses
           CALL $GTHST
           IFSKP. <RET>        ; we are a relay for this name
           AOJA E,TOP.         ; consider next relay
         ENDDO.
       ENDIF.
       RETSKP                  ; not a relay

       ENDSV.

; Get TCP location.  Skips if a TCP connection

IFNDEF TCP%TV,TCP%TV==:1B11     ; TVT argument supplied
IFNDEF $TFH,$TFH==:7            ; TCB foreign address
IFNDEF $TLH,$TLH==:10           ; TCB local address

GETTCP: IFQN. F%NVT             ; NVT server?
         MOVX A,TCP%TV         ; argument is TVT
         HRR A,MYTTYN          ; our TVT number
         HRROI B,$TFH          ; want host number
         HRROI C,FRNHNO        ; put it in FRNHNO
         STAT%
          ERJMP R
         MOVX A,TCP%TV         ; argument is TVT
         HRR A,MYTTYN          ; our TVT number
         HRROI B,$TLH          ; want local host address
         HRROI C,LCLHNO        ; put it in LCLHNO
         STAT%                 ; get it
          ERJMP R
       ELSE.
         MOVX A,.PRIIN         ; get foreign host from TCB
         MOVX B,.TCRTW
         MOVEI C,$TFH
         TCOPR%
          ERJMP R
         MOVEM C,FRNHNO        ; save foreign host address
         MOVEI C,$TLH          ; now get local host
         TCOPR%
          ERJMP R
         MOVEM C,LCLHNO        ; save local host address
       ENDIF.
       HRROI A,LCLHST          ; get local host name
       SETO B,
       CALL $GTHNS
        RET
       HRROI A,LCLHST          ; remove relative domain from name
       CALL $RMREL
       MOVEM B,LCLHNC          ; save canonical local host address
       CAMN B,LCLHNO           ; same as local host address?
       IFSKP.
         HRROI A,BUFFER        ; ugh, gotta look at this closer
         MOVE B,LCLHNO         ; get name from connection local address
         CALL $GTHNS
       ANSKP.
         HRROI A,BUFFER        ; remove relative domain from name
         CALL $RMREL
         HRROI A,LCLHST        ; compare the names
         HRROI B,BUFFER
         STCMP%
       ANDN. A
         TMSG <421->           ; sorry, local ports not supported yet!!
         HRROI A,BUFFER        ; output host name
         PSOUT%
         TMSG < ESMTP service isn't operational yet
421 >
         JRST QUIT1
       ENDIF.
       MOVX A,P%TCP            ; set protocol to be TCP
       STOR A,F%PRO
       RETSKP

; Get Chaos location.  Skips if a Chaosnet connection.

IFNDEF .MOFHS,<.MOFHS==34>      ; foreign host# from Chaosnet JFN

GETCHA: HRROI A,[ASCIZ/CHA:/]   ; see if we know what Chaosnet is
       STDEV%                  ; (can't use .DVCHA since not constant)
        ERJMP R                ; guess not
       MOVE D,B                ; save device designator for comparison
       MOVX A,.PRIIN           ; see if primary I/O is Chaosnet
       DVCHR%                  ; (assume .PRIOU is if .PRIIN is)
        ERJMP R
       CAME A,D                ; is it Chaos/SMTP?
        RET
       MOVX A,.PRIIN           ; yes, get foreign host number
       MOVX B,.MOFHS
       MTOPR%
        ERCAL FATAL
       MOVEM C,FRNHNO          ; save host number
       HRROI A,FRNHST          ; look up the name
       MOVE B,C                ; host number
       CALL $CHSNS             ; use HSTNAM, just in case CHAOS uses domains
       IFNSK.
         TMSG <421-Unable to get foreign host name>
         JRST IMPERR
       ENDIF.
       HRROI A,LCLHST          ; get local host name and address
       SETO B,
       CALL $CHSNS
       IFNSK.
         TMSG <421-Unable to get local host name>
         JRST IMPERR
       ENDIF.
       MOVEM B,LCLHNO          ; $CHSNS returns local address too
       MOVEM B,LCLHNC          ; and it's always the cannonical address
       MOVX A,P%CHA            ; Set protocol to be Chaos
       STOR A,F%PRO
       TQO F%JFN               ; Remember that we have to close JFN
       RETSKP
      SUBTTL Error handling

; Common routine called to output last error code's message

ERROUT: MOVX A,.PRIOU
       HRLOI B,.FHSLF          ; dumb ERSTR%
       SETZ C,
       ERSTR%
        JRST ERRUND            ; undefined error number
        NOP                    ; can't happen
       RET

ERRUND: TMSG <Undefined error >
       MOVX A,.FHSLF           ; get error number
       GETER%
       MOVX A,.PRIOU           ; output it
       HRRZS B                 ; only right half where error code is
       MOVX C,^D8              ; in octal
       NOUT%
        ERJMP R                ; ignore error here
       RET

; Various SMTP errors

BADCMD: TMSG <500 Command unrecognized: >
       JRST DMPCMD

SYNFLD: TMSG <500 Syntax error or field too long: >
       JRST DMPCMD

SYNERR: TMSG <500 Syntax error in command: >
       JRST DMPCMD

NOVREX:
IFN FT2821,<                    ; do different from NOTIMP if RFC 2821
       TMSG <252 Sorry, we do not allow this operation>
       JRST GETCMD
>;IFN FT2821
NOTIMP: TMSG <502 Command not implemented: >
       JRST DMPCMD

HLOREQ: TMSG <503 HELO required before starting a transaction: >
       JRST DMPCMD

MAIREQ: TMSG <503 MAIL FROM required before recipients: >
       JRST DMPCMD

RCPREQ: TMSG <503 RCPT TO required before data: >
       JRST DMPCMD

INPROG: TMSG <503 >
       LOAD A,F%DOP            ; get current delivery option
       HRROI A,DOPTAB(A)       ; output name of current delivery option
       PSOUT%
       TMSG < already in progress, must RSET first: >
       JRST DMPCMD

MISARG: TMSG <500 Missing required argument: >
       JRST DMPCMD

BADARG: TMSG <500 Argument given when none expected: >
DMPCMD: HRROI A,BUFFER          ; output losing command
       PSOUT%
       JRST GETCMD

; Fatal errors arrive here

FATAL:  MOVEM 17,FATACS+17      ; save ACs in FATACS for debugging
       MOVEI 17,FATACS         ; save from 0 => FATACS
       BLT 17,FATACS+16        ; ...to 16 => FATACS+16
       MOVE 17,FATACS+17       ; restore AC17
       MOVX A,.PRIIN           ; flush TTY input
       CFIBF%
        ERJMP .+1
       CALL CRLF               ; new line first
       TMSG <421-Fatal system error: >
       CALL ERROUT             ; output last JSYS error
       TMSG <, >
       MOVE F,(P)              ; get PC
       MOVE F,-2(F)            ; get instruction which lost
       CALL SYMOUT             ; output symbolic instruction if possible
       TMSG < at PC >
       POP P,F
       MOVEI F,-2(F)           ; point PC at actual location of the JSYS
       CALL SYMOUT             ; output symbolic name of the PC

; Entry point to ask for a report for non-JSYS "impossible" error

IMPERR: CALL CRLF
       TMSG <421-This isn't expected to happen; please report this
421 >
       JRST QUIT1              ; skip over 221 reply code in QUIT code

;  Clever symbol table lookup routine.  For details, read "Introduction to
; DECSYSTEM-20 Assembly Language Programming", by Ralph Gorin, published by
; Digital Press, 1981.  Called with desired value in F.

SYMOUT: SETZB C,E               ; no current program name or best symbol
       MOVE D,.JBSYM           ; symbol table pointer
       HLRO A,D
       SUB D,A                 ; -count,,ending address +1
       DO.
         LDB A,[POINT 4,-2(D),3] ; symbol type
         IFN. A                ; 0=prog name (uninteresting)
           CAILE A,2           ; 1=global, 2=local
         ANSKP.
           MOVE A,-1(D)        ; value of the symbol
           CAME A,F            ; exact match?
           IFSKP.
             MOVE E,D          ; yes, select it as best symbol
             EXIT.
           ENDIF.
           CAML A,F            ; smaller than value sought?
         ANSKP.
           SKIPE B,E           ; get best one so far if there is one
            CAML A,-1(B)       ; compare to previous best
             MOVE E,D          ; current symbol is best match so far
         ENDIF.
         ADD D,[2000000-2]     ; add 2 in the left, sub 2 in the right
         JUMPL D,TOP.          ; loop unless control count is exhausted
       ENDDO.
       IFN. E                  ; if a best symbol found
         MOVE A,F              ; desired value
         SUB A,-1(E)           ; less symbol's value = offset
         CAIL A,200            ; is offset small enough?
       ANSKP.
         MOVE A,-2(E)          ; symbol name
         TXZ A,<MASKB 0,3>     ; clear flags
         CALL SQZTYO           ; print symbol name
         SUB F,-1(E)           ; difference between this and symbol's value
         JUMPE F,R             ; if no offset then done
         MOVX A,"+"            ; add + to the output line
         PBOUT%
       ENDIF.
       MOVX A,.PRIOU           ; and copy numeric offset to output
       MOVE B,F                ; value to output
       MOVX C,^D8
       NOUT%
        ERJMP R
       RET
      SUBTTL Interrupt stuff

; PSI blocks

LEVTAB: LEV1PC                  ; priority level table
       LEV2PC
       LEV3PC

CHNTAB: PHASE 0                 ; channel table
COFCHN:!1,,COFINT               ; carrier off channel
TIMCHN:!2,,TIMINT               ; timer channel
       REPEAT ^D36-.,<0>
       DEPHASE

; Set up PSIs

SETPSI: MOVX A,.FHSLF           ; set level/channel tables
       MOVE B,[LEVTAB,,CHNTAB]
       SIR%
        ERCAL FATAL
       EIR%                    ; enable PSIs
        ERCAL FATAL
       MOVX B,<1B<TIMCHN>!1B<COFCHN>> ; on these channels
       AIC%
        ERCAL FATAL
       MOVX A,<XWD .TICRF,COFCHN> ; arm for carrier off interrupts
       ATI%
;       CALLRET SETTIM

; Initialize the timer

SETTIM: MOVE A,[.FHSLF,,.TIMAL] ; remote all pending timers
       TIMER%
        ERCAL FATAL
       MOVE A,[.FHSLF,,.TIMEL] ; tick the timer every 15 seconds
       MOVX B,^D15*^D1000
       MOVX C,TIMCHN
       TIMER%
        ERCAL FATAL
       RET

; Timer interrupt

TIMINT: DMOVEM A,IN2ACS         ; save ACs
       MOVEM C,IN2ACS+2
       AOSE TIMOUT             ; has timer run out yet?
       IFSKP.
         MOVX A,.PRIIN         ; flush TTY input
         CFIBF%
          ERJMP .+1
         CALL CRLF             ; output CRLF
         TMSG <421-Autologout; idle for too long
421 >
         MOVX A,<PC%USR!QUIT1> ; dismiss to quit code
         MOVEM A,LEV2PC
       ELSE.
         CALL SETTIM           ; reinitialize the timer
       ENDIF.
       DMOVE A,IN2ACS          ; restore ACs
       MOVE C,IN2ACS+2
       DEBRK%

; Carrier-off interrupt

COFINT: CALL HANGUP             ; hang up the connection
       DEBRK%                  ; back out if continued
      SUBTTL Other randomness

; Literals

..VAR:!VAR                      ; generate variables (there shouldn't be any)
IFN .-...VAR,<.FATAL Variables illegal in this program>
..LIT:  XLIST                   ; save trees during LIT
       LIT                     ; generate literals
       LIST

       END EVECL,,EVEC         ; The End