diff --git a/Doxyfile b/Doxyfile index 508f65409f5b62374bf2ce568252f03c0d621971..ad35549f978f7a7d05472f7beaa6e5aabb414dea 100644 --- a/Doxyfile +++ b/Doxyfile @@ -552,8 +552,7 @@ WARN_LOGFILE = # directories like "/usr/src/myproject". Separate the files or directories # with spaces. -INPUT = mime.py \ - Milter +INPUT = mime.py doc/mainpage.py doc/milter.py Milter # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding, which is diff --git a/Milter/__init__.py b/Milter/__init__.py index ab8a3dec6be6586c17796213e4879d5befa72168..960cad10c5375fd7a081548b74de230c40ad042e 100755 --- a/Milter/__init__.py +++ b/Milter/__init__.py @@ -4,7 +4,7 @@ # Clients generally subclass Milter.Base and define callback # methods. # -# author Stuart D. Gathman <stuart@bmsi.com> +# @author Stuart D. Gathman <stuart@bmsi.com> # Copyright 2001,2009 Business Management Systems, Inc. # This code is under the GNU General Public License. See COPYING for details. @@ -224,6 +224,7 @@ class Base(object): # RCPT TO command (and as delivered to the envrcpt callback), for example # "self.addrcpt('<foo@example.com>')". # @param rcpt the message recipient + # @param params an optional list of ESMTP parameters def addrcpt(self,rcpt,params=None): if not self._actions & ADDRCPT: raise DisabledAction("ADDRCPT") return self._ctx.addrcpt(rcpt,params) @@ -242,6 +243,12 @@ class Base(object): if not self._actions & MODBODY: raise DisabledAction("MODBODY") return self._ctx.replacebody(body) + ## Change the SMTP envelope sender address. + # The syntax of the sender is that same as used in the SMTP + # MAIL FROM command (and as delivered to the envfrom callback), + # for example <code>self.chgfrom('<bar@example.com>')</code>. + # @param sender the new sender address + # @param params an optional list of ESMTP parameters def chgfrom(self,sender,params=None): if not self._actions & CHGFROM: raise DisabledAction("CHGFROM") return self._ctx.chgfrom(sender,params) @@ -325,13 +332,19 @@ class Milter(Base): self.log("close") return CONTINUE +## The milter connection factory +# This factory method is called for each connection to create the +# python object that tracks the connection. It should return +# an object derived from Milter.Base. factory = Milter +## @private def negotiate_callback(ctx,opts): m = factory() m._setctx(ctx) return m.negotiate(opts) +## @private def connect_callback(ctx,hostname,family,hostaddr,nr_mask=P_NR_CONN): m = ctx.getpriv() if not m: @@ -341,6 +354,7 @@ def connect_callback(ctx,hostname,family,hostaddr,nr_mask=P_NR_CONN): m._setctx(ctx) return m.connect(hostname,family,hostaddr) +## @private def close_callback(ctx): m = ctx.getpriv() if not m: return CONTINUE @@ -350,8 +364,10 @@ def close_callback(ctx): m._setctx(None) # release milterContext return rc +## Convert ESMTP parameters with values to a keyword dictionary. +# @deprecated You probably want Milter.param2dict instead. def dictfromlist(args): - "Convert ESMTP parm list to keyword dictionary." + "Convert ESMTP parms with values to keyword dictionary." kw = {} for s in args: pos = s.find('=') @@ -359,6 +375,16 @@ def dictfromlist(args): kw[s[:pos].upper()] = s[pos+1:] return kw +## Convert ESMTP parm list to keyword dictionary. +# Params with no value are set to None in the dictionary. +# @param str list of param strings of the form "NAME" or "NAME=VALUE" +def param2dict(str): + "Convert ESMTP parm list to keyword dictionary." + pairs = [x.split('=',1) for x in str] + for e in pairs: + if len(e) < 2: e.append(None) + return dict([(k.upper(),v) for k,v in pairs]) + def envcallback(c,args): """Call function c with ESMTP parms converted to keyword parameters. Can be used in the envfrom and/or envrcpt callbacks to process @@ -373,6 +399,22 @@ def envcallback(c,args): pargs.append(s) return c(*pargs,**kw) +## Run the milter. +# The MTA can communicate with the milter by means of a +# unix, inet, or inet6 socket. By default, a unix domain socket +# is used. It must not exist, +# and sendmail will throw warnings if, eg, the file is under a +# group or world writable directory. +# <pre> +# setconn('unix:/var/run/pythonfilter') +# setconn('inet:8800') # listen on ANY interface +# setconn('inet:7871@@publichost') # listen on a specific interface +# setconn('inet6:8020') +# </pre> +# @param name the name of the milter known by the MTA +# @param socketname the descriptor of the unix socket +# @param timeout the time in secs the MTA should wait for a response before +# considering this milter dead def runmilter(name,socketname,timeout = 0): # This bit is here on the assumption that you will be starting this filter # before sendmail. If sendmail is not running and the socket already exists, diff --git a/doc/mainpage.py b/doc/mainpage.py new file mode 100644 index 0000000000000000000000000000000000000000..e63f2b28db8874554c1d1fb12708e490e729a0d2 --- /dev/null +++ b/doc/mainpage.py @@ -0,0 +1,36 @@ +## @mainpage Writing Milters in Python +# +# +# At the lowest level, the <code>milter</code> module provides a thin wrapper +# around the <a href="https://www.milter.org/developers/api/index"> sendmail +# libmilter API</a>. This API lets you register callbacks for a number of +# events in the process of sendmail receiving a message via SMTP. These +# events include the initial connection from a MTA, the envelope sender and +# recipients, the top level mail headers, and the message body. There are +# options to mangle all of these components of the message as it passes through +# the milter. +# +# At the next level, the <code>Milter</code> module (note the case difference) +# provides a Python friendly object oriented wrapper for the low level API. To +# use the Milter module, an application registers a 'factory' to create an +# object for each connection from a MTA to sendmail. These connection objects +# must provide methods corresponding to the libmilter callback events. +# +# Each event method returns a code to tell sendmail whether to proceed with +# processing the message. This is a big advantage of milters over other mail +# filtering systems. Unwanted mail can be stopped in its tracks at the +# earliest possible point. +# +# The <code>Milter.Base</code> class provides default implementations for +# event methods that do nothing, and also provides wrappers for the libmilter +# methods to mutate the message. It automatically negotiates with MTA +# which protocol steps need to be processed by the milter, based on +# which callback methods are overridden. +# +# The <code>Milter.Milter</code> class provides an alternate default +# implementation that logs the main milter events, but otherwise does nothing. +# It is provided for compatibility. +# +# The <code>mime</code> module provides a wrapper for the Python email package +# that fixes some bugs, and simplifies modifying selected parts of a MIME +# message. diff --git a/doc/milter.py b/doc/milter.py new file mode 100644 index 0000000000000000000000000000000000000000..bcd8ee6b0eb83556ebee318e4906ed13c0307c1d --- /dev/null +++ b/doc/milter.py @@ -0,0 +1,44 @@ +# Document miltermodule for Doxygen +# + +## @package milter +# +# A thin wrapper around libmilter. +# + +class milterContext(object): + def getsymval(self,sym): pass + def setreply(self,rcode,xcode,*msg): pass + def addheader(self,name,value,idx=-1): pass + def chgheader(self,name,idx,value): pass + def addrcpt(self,rcpt,params=None): pass + def delrcpt(self,rcpt): pass + def replacebody(self,data): pass + def setpriv(self,priv): pass + def getpriv(self): pass + def quarantine(self,reason): pass + def progress(self): pass + def chgfrom(self,sender,param=None): pass + def setsmlist(self,stage,macrolist): pass + +class error(Exception): pass + +def set_flags(flags): pass +def set_connect_callback(cb): pass +def set_helo_callback(cb): pass +def set_envfrom_callback(cb): pass +def set_envrcpt_callback(cb): pass +def set_header_callback(cb): pass +def set_eoh_callback(cb): pass +def set_body_callback(cb): pass +def set_abort_callback(cb): pass +def set_close_callback(cb): pass +def set_exception_policy(code): pass +def register(name,negotiate=None,unknown=None,data=None): pass +def opensocket(rmsock): pass +def main(): pass +def setdbg(lev): pass +def settimeout(secs): pass +def setbacklog(n): pass +def setconn(s): pass +def stop(): pass diff --git a/mime.py b/mime.py index 6ceb6bdadf12cc7af80ad835242bbd508323919b..2c8b5304539d86adeb386da3e87db8c12dfcde13 100644 --- a/mime.py +++ b/mime.py @@ -1,4 +1,7 @@ # $Log$ +# Revision 1.5 2005/07/20 14:49:43 customdesigned +# Handle corrupt and empty ZIP files. +# # Revision 1.4 2005/06/17 01:49:39 customdesigned # Handle zip within zip. # @@ -70,8 +73,12 @@ # with old milter code. # -# This module provides a "defang" function to replace naughty attachments -# with a warning message. +## @package mime +# This module provides a "defang" function to replace naughty attachments. +# +# We also provide workarounds for bugs in the email module that comes +# with python. The "bugs" fixed mostly come up only with malformed +# messages - but that is what you have when dealing with spam. # Author: Stuart D. Gathman <stuart@bmsi.com> # Copyright 2001,2002,2003,2004,2005 Business Management Systems, Inc. @@ -93,6 +100,8 @@ from email import Errors from types import ListType,StringType +## Return a list of filenames in a zip file. +# Embedded zip files are recursively expanded. def zipnames(txt): fp = StringIO.StringIO(txt) zipf = zipfile.ZipFile(fp,'r') @@ -103,6 +112,8 @@ def zipnames(txt): names += zipnames(zipf.read(nm)) return names +## Fix multipart handling in email.Generator. +# class MimeGenerator(Generator): def _dispatch(self, msg): # Get the Content-Type: for the message, then try to dispatch to @@ -142,11 +153,8 @@ def _unquotevalue(value): from email.Message import _parseparam -# Enhance email.Message -# - Provide a headerchange event for integration with Milter -# Headerchange attribute can be assigned a function to be called when -# changing headers. The signature is: -# headerchange(msg,name,value) -> None +## Enhance email.Message +# # - Track modifications to headers of body or any part independently class MimeMessage(Message): @@ -158,6 +166,12 @@ class MimeMessage(Message): self.submsg = None self.modified = False + ## @var headerchange + # Provide a headerchange event for integration with Milter. + # The headerchange attribute can be assigned a function to be called when + # changing headers. The signature is: + # headerchange(msg,name,value) -> None + def get_param(self, param, failobj=None, header='content-type', unquote=True): val = Message.get_param(self,param,failobj,header,unquote) if val != failobj and param == 'boundary' and unquote: