diff --git a/Doxyfile b/Doxyfile index ad35549f978f7a7d05472f7beaa6e5aabb414dea..fecd1229decb983c079aeb4e97ca0f91f89d4b90 100644 --- a/Doxyfile +++ b/Doxyfile @@ -31,7 +31,7 @@ PROJECT_NAME = pymilter # This could be handy for archiving the generated documentation or # if some version control system is used. -PROJECT_NUMBER = 0.9.2 +PROJECT_NUMBER = 0.9.3 # The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) # base path where the generated documentation will be put. diff --git a/Milter/__init__.py b/Milter/__init__.py index 960cad10c5375fd7a081548b74de230c40ad042e..f712fe0b3ef8ff0c278bccbd54864c22c8f80f1f 100755 --- a/Milter/__init__.py +++ b/Milter/__init__.py @@ -146,6 +146,14 @@ class Base(object): def close(self): return CONTINUE ## Return mask of SMFIP_N.. protocol option bits to clear for this class + # The @@nocallback and @@noreply decorators set the + # <code>milter_protocol</code> function attribute to the protocol mask bit to + # pass to libmilter, causing that callback or its reply to be skipped. + # Overriding a method creates a new function object, so that + # <code>milter_protocol</code> defaults to 0. + # Libmilter passes the protocol bits that the current MTA knows + # how to skip. We clear the ones we don't want to skip. + # The negation is somewhat mind bending, but it is simple. @classmethod def protocol_mask(klass): try: diff --git a/Milter/dns.py b/Milter/dns.py index ff962c26032aa08ce128d155d45f342f2c41d8be..fe9b54a2aeb88b99b5e5c9b667a7516e2858c47a 100644 --- a/Milter/dns.py +++ b/Milter/dns.py @@ -1,12 +1,22 @@ -# provide a higher level interface to pydns +## @package Milter.dns +# Provide a higher level interface to pydns. import DNS from DNS import DNSError MAX_CNAME = 10 +## Lookup DNS records by label and RR type. +# The response can include records of other types that the DNS +# server thinks we might need. +# @param name the DNS label to lookup +# @param qtype the name of the DNS RR type to lookup +# @return a list of ((name,type),data) tuples def DNSLookup(name, qtype): try: + # To be thread safe, we create a fresh DnsRequest with + # each call. It would be more efficient to reuse + # a req object stored in a Session. req = DNS.DnsRequest(name, qtype=qtype) resp = req.req() #resp.show() @@ -24,25 +34,28 @@ class Session(object): def __init__(self): self.cache = {} + ## Additional DNS RRs we can safely cache. # We have to be careful which additional DNS RRs we cache. For # instance, PTR records are controlled by the connecting IP, and they # could poison our local cache with bogus A and MX records. + # Each entry is a tuple of (query_type,rr_type). So for instance, + # the entry ('MX','A') says it is safe (for milter purposes) to cache + # any 'A' RRs found in an 'MX' query. + SAFE2CACHE = frozenset(( + ('MX','MX'), ('MX','A'), + ('CNAME','CNAME'), ('CNAME','A'), + ('A','A'), + ('AAAA','AAAA'), + ('PTR','PTR'), + ('NS','NS'), ('NS','A'), + ('TXT','TXT'), + ('SPF','SPF') + )) - SAFE2CACHE = { - ('MX','A'): None, - ('MX','MX'): None, - ('CNAME','A'): None, - ('CNAME','CNAME'): None, - ('A','A'): None, - ('AAAA','AAAA'): None, - ('PTR','PTR'): None, - ('NS','NS'): None, - ('NS','A'): None, - ('TXT','TXT'): None, - ('SPF','SPF'): None - } - - + ## Cached DNS lookup. + # @param name the DNS label to query + # @param qtype the query type, e.g. 'A' + # @param cnames tracks CNAMES already followed in recursive calls def dns(self, name, qtype, cnames=None): """DNS query. diff --git a/Milter/dsn.py b/Milter/dsn.py index 75be7ff53a359b5c1a4440e943913adbea8a4e3e..290fdd0b8ff0e7179c057950798e7ef6a649f90a 100644 --- a/Milter/dsn.py +++ b/Milter/dsn.py @@ -5,6 +5,9 @@ # Send DSNs, do call back verification, # and generate DSN messages from a template # $Log$ +# Revision 1.17 2009/05/20 20:08:44 customdesigned +# Support non-DSN CBV (non-empty MAIL FROM) +# # Revision 1.16 2007/09/25 01:24:59 customdesigned # Allow arbitrary object, not just spf.query like, to provide data for create_msg # @@ -26,7 +29,31 @@ # Revision 1.10 2006/05/24 20:56:35 customdesigned # Remove default templates. Scrub test. # - +## @package Milter.dsn +# Support DSNs and CallBackValidations (CBV). +# +# A Delivery Status Notification (bounce) is sent to the envelope +# sender (original MAIL FROM) with a null MAIL FROM (<>) to notify the +# original sender # of delays or problems with delivery. A Callback Validation +# starts the DSN process, but stops before issuing the DATA command. The +# purpose is to check whether the envelope recipient is accepted (and is +# therefore a valid email). The null MAIL FROM tells the remote +# MTA to never reply according to RFC2821 (but some braindead MTAs +# reply anyway, of course). +# +# Milters should cache CBV results and should avoid sending DSNs +# unless the sender is authenticated somehow (e.g. SPF Pass). However, +# when email is quarantined, and is not known to be a forgery, sending a DSN +# is better than silently disappearing, and a DSN is better than sending +# a normal message as notification - because MAIL FROM signing schemes +# can reject bounces of forged emails. Whatever you do, don't copy those +# assinine commercial filters that send a normal message to notify you +# that some virus is forging your email. +# +# <b>DSNs should *only* be sent to MAIL FROM addresses.</b> Never send +# a DSN or use a null MAIL FROM with an email address obtained from +# anywhere else. +# import smtplib import socket from email.Message import Message @@ -34,6 +61,19 @@ import Milter import time import dns +## Send DSN. +# Try the published MX names in order, rejecting obviously bogus entries +# (like <code>localhost</code>). +# @param mailfrom the original sender we are notifying or validating +# @param receiver the HELO name of the MTA we are sending the DSN on behalf of. +# Be sure to send from an IP that matches the HELO. +# @param msg the DSN message in RFC2822 format, or None for CBV. +# @param timeout total seconds to wait for a response from an MX +# @param session Milter.dns.Session object from current incoming mail +# session to reuse its cache, or None to create a fresh one. +# @param ourfrom set to a valid email to send a normal notification from, or +# to validate emails not obtained from MAIL FROM. +# @return None on success or (status_code,msg) on failure. def send_dsn(mailfrom,receiver,msg=None,timeout=600,session=None,ourfrom=''): """Send DSN. If msg is None, do callback verification. Mailfrom is original sender we are sending DSN or CBV to.