ietf-clear
[Top] [All Lists]

[clear] pyCSV

2005-08-17 18:24:10
David MacQuigg <dmquigg-clear(_at_)yahoo(_dot_)com> wrote:

Following is the first draft of my script to check CSV records, and it 
seems to work correctly on the examples in the docstrings.  The script was 
written with Sendmail in mind, but it should interface easily with just 
about any MTA, since the return values are pretty much universal.

Suggestions are welcome, particularly anything I haven't thought of.

# pyCSV.py  8/16/05  David MacQuigg

   I assume this is written in Python, which I'm not familiar with.
(So my comments may be off-base, alas...)

def csv(IP, helo):

   I assume "IP" is the remote IP address of the TCP connection, and
"helo" is the string from the HELO/EHLO command.

    Returns ( action, SMTP_reply, header )

      action: 'ACCEPT', 'REJECT', 'TEMPFAIL'

   CSV most often produces I result I call "unknown", in that the IP
address is neither known to be an authorized SMTP client nor known to
not be an authorized SMTP client. There are several ways this can happen.

      SMTP_reply = ( SMTP_code, Xcode, explanation )
        SMTP_code: SMTP Reply Code per RFC-2821
        Xcode:  Enhanced Mail System Status Code per RFC-3463

      header = {'label': 'Authent:',
                 'text': '%s %s CSV %s' % (IP, helo, result) }
        result: 'PASS', 'FAIL'

Examples:
csv('168.61.5.27', 'harry.mail-abuse.org')
('ACCEPT', (250, '', 'Sender CSV OK'), \
[{'text': '168.61.5.27 harry.mail-abuse.org CSV PASS', 'label': 
'Authent:'}])

   This is an "authenticated and authorized" case, deserving the full
trust that local policy assigns to "harry.mail-abuse.org".

csv('192.168.0.64', 'harry.mail-abuse.org')
('REJECT', (550, '5.7.1', \
"'192.168.0.64' not authorized by 'harry.mail-abuse.org'"), [])

   This is a "client is not authenticated" case, where a "complete"
list if IP addresses for SMTP clients authorized to use the HELO string
"harry.mail-abuse.org" was returned, but the actual IP address of the
connection is not one of them. Rejection of all email is appropriate.

csv(IP, 'yahoo.com')
('REJECT', (550, '5.7.1', "No SRV record for '_client._smtp.yahoo.com'."), 
[])

   This is an "unknown" case, where no SRV record is published. CSV
yields no information on whether the SMTP client issuing that HELO is
authorized or not. Rejection of the email is NOT appropriate.

    ## Get an SRV record for the helo name:
    name = '_client._smtp.' + helo
    try:
        reqobj = DNS.Request(name, qtype='SRV', timeout=DNS.timeout)
        resp = reqobj.req()

    except DNSError, expln:
        exp = str(expln)
        if exp == 'Timeout':
            msg = ("Timeout getting SRV record for '%s'.\n" % name
                 + "Try again later."  )
            return ('TEMPFAIL', (450, '', msg), [] )

   This differs (slightly) from the "unknown" case, in that there might
be a SRV record, and a later retry might yield a different result. It
is reasonable to return a temporary error or to treat this as "unknown".

        else:
            msg == exp + "\nDNS error getting SRV record for '%s'" % name
            return ('REJECT', (550, '?.?.?', msg), [] )

   This is a case of messed-up DNS. CSV expresses no opinion on what
is the most reasonable action; but I personally tend towards treating
it as an "unknown" case.

    ## Check for too few or too many SRV records:
    lr = len(resp.answers)
    if lr == 0:
        exp = "No SRV record for '%s'." % name   # or non-existent domain
        return ('REJECT', (550, '5.7.1', exp), [])

   I don't know what the DNS.Request response format is. No SRV RR is
a normal case, and should yield an "unknown" result.

    if lr  > 1:
        exp = "Found %s SRV records for '%s'. Should be 1." % (lr, name)
        return ('REJECT', (550, '5.7.1', exp), [])

   This does not allow for multiple CSV versions (distinguished by the
"priority" field. It would be better to count the priority==1 records.
(More than one of those _is_ an error.)

    ## Extract the needed info from the response:
    ra0 = resp.answers[0]
    rad = ra0['data']
    priority = rad[0]  # CSV version

   This should be checked. The recommended action is to ignore all
SRV records with a version you don't know (currently only version 1
is knowable).

    weight   = rad[1]  # authorization ( 1 = NO, 2 = YES )
    port     = rad[2]  # subdomain authorization
                       # ( 0 = unknown, 1 = CSV required )
    target   = rad[3]  # authorized hostname (ID)

    if weight != 2:
        exp = "'%s' not authorized to send mail" % target
        return ('REJECT', (550, '5.7.1', exp), [])

   Hopefully, weight==3 won't be a common case, but you should allow
for it. It means that some SMTP client(s) are authorized to use that
HELO string, but the list of IP addresses is not available in DNS.
Thus, weight==3 is another "unknown" case.

    ## Check the A records for the authorized name:
    try:
        reqobj = DNS.Request(target, qtype='A', timeout=DNS.timeout)
        resp = reqobj.req()

   (This, of course, doesn't work for IPv6.)

    except DNSError, expln:
        exp = str(expln)
        if exp == 'Timeout':
            msg = ("Timeout getting A records for '%s'.\n" % target
                 + "Try again later."  )
            return ('TEMPFAIL', (450, '', msg), [] )

   A temporary SMTP error seems the most appropriate here.

        else:
            msg == exp + "\nDNSError getting A records for '%s'" % target
            return ('REJECT', (550, '?.?.?', msg), [] )

   This is another messed-up DNS situation. Again, CSV expresses no
opinion on what action is most reasonable.

    ## Make a list of the authorized IP addresses:
    aa = []
    for ans in resp.answers:
        aa.append(ans['data'])

    ## Check incoming IP against the list:
    if IP in aa:
        action = 'ACCEPT'
        SMTP_reply = (250, '', 'Sender CSV OK')
        header = {'label': 'Authent:',
                    'text': '%s %s CSV PASS' % (IP, helo) }

   In principle, I quite approve of adding a header; however, the
devil is in the details...

        return (action, SMTP_reply, [header] )

   This is the "authenticated and authorized" case.

    else:
        action = 'REJECT'
        SMTP_reply = (550, '5.7.1',
            "'%s' not authorized by '%s'" % (IP, helo) )
        return (action, SMTP_reply, [] )

   Here we've collected a complete list (assuming DNS.Request works
the way I guess it does), and the actual IP address used is not on
the list. Rejection of email is appropriate.

   IMHO, it would be better to design this routine to have an "unknown"
return action.

   Also note that this routine makes no attempt to search for parent
domains which specify whether CSV records are published for all the
subdomains which are authorized SMTP servers. No such search is
_required_ by the spec, but it is helpful to have some way of detecting
this case.

--
John Leslie <john(_at_)jlc(_dot_)net>
<Prev in Thread] Current Thread [Next in Thread>